diff --git a/app/code/Magento/Braintree/Test/Unit/Model/InstantPurchase/CreditCard/AvailabilityCheckerTest.php b/app/code/Magento/Braintree/Test/Unit/Model/InstantPurchase/CreditCard/AvailabilityCheckerTest.php
new file mode 100644
index 0000000000000..2248aab1aad2e
--- /dev/null
+++ b/app/code/Magento/Braintree/Test/Unit/Model/InstantPurchase/CreditCard/AvailabilityCheckerTest.php
@@ -0,0 +1,70 @@
+configMock = $this->createMock(Config::class);
+ $this->availabilityChecker = new AvailabilityChecker($this->configMock);
+ }
+
+ /**
+ * Test isAvailable method
+ *
+ * @dataProvider isAvailableDataProvider
+ *
+ * @param bool $isVerify3DSecure
+ * @param bool $expected
+ *
+ * @return void
+ */
+ public function testIsAvailable(bool $isVerify3DSecure, bool $expected)
+ {
+ $this->configMock->expects($this->once())->method('isVerify3DSecure')->willReturn($isVerify3DSecure);
+ $actual = $this->availabilityChecker->isAvailable();
+ self::assertEquals($expected, $actual);
+ }
+
+ /**
+ * Data provider for isAvailable method test
+ *
+ * @return array
+ */
+ public function isAvailableDataProvider()
+ {
+ return [
+ [true, false],
+ [false, true],
+ ];
+ }
+}
diff --git a/app/code/Magento/Braintree/Test/Unit/Model/InstantPurchase/CreditCard/TokenFormatterTest.php b/app/code/Magento/Braintree/Test/Unit/Model/InstantPurchase/CreditCard/TokenFormatterTest.php
new file mode 100644
index 0000000000000..a5c7cd743d85f
--- /dev/null
+++ b/app/code/Magento/Braintree/Test/Unit/Model/InstantPurchase/CreditCard/TokenFormatterTest.php
@@ -0,0 +1,119 @@
+ 'visa',
+ 'maskedCC' => '1111************9999',
+ 'expirationDate' => '01-01-2020'
+ ];
+
+ /**
+ * Set Up
+ *
+ * @return void
+ */
+ protected function setUp()
+ {
+ $this->paymentTokenMock = $this->getMockBuilder(PaymentTokenInterface::class)
+ ->getMockForAbstractClass();
+
+ $this->creditCardTokenFormatter = new CreditCardTokenFormatter();
+ }
+
+ /**
+ * Testing the payment format with a known credit card type
+ *
+ * @return void
+ */
+ public function testFormatPaymentTokenWithKnownCardType()
+ {
+ $this->tokenDetails['type'] = key(CreditCardTokenFormatter::$baseCardTypes);
+ $this->paymentTokenMock->expects($this->once())
+ ->method('getTokenDetails')
+ ->willReturn(json_encode($this->tokenDetails));
+
+ $formattedString = sprintf(
+ '%s: %s, %s: %s (%s: %s)',
+ __('Credit Card'),
+ reset(CreditCardTokenFormatter::$baseCardTypes),
+ __('ending'),
+ $this->tokenDetails['maskedCC'],
+ __('expires'),
+ $this->tokenDetails['expirationDate']
+ );
+
+ self::assertEquals(
+ $formattedString,
+ $this->creditCardTokenFormatter->formatPaymentToken($this->paymentTokenMock)
+ );
+ }
+
+ /**
+ * Testing the payment format with a unknown credit card type
+ *
+ * @return void
+ */
+ public function testFormatPaymentTokenWithUnknownCardType()
+ {
+ $this->paymentTokenMock->expects($this->once())
+ ->method('getTokenDetails')
+ ->willReturn(json_encode($this->tokenDetails));
+
+ $formattedString = sprintf(
+ '%s: %s, %s: %s (%s: %s)',
+ __('Credit Card'),
+ $this->tokenDetails['type'],
+ __('ending'),
+ $this->tokenDetails['maskedCC'],
+ __('expires'),
+ $this->tokenDetails['expirationDate']
+ );
+
+ self::assertEquals(
+ $formattedString,
+ $this->creditCardTokenFormatter->formatPaymentToken($this->paymentTokenMock)
+ );
+ }
+
+ /**
+ * Testing the payment format with wrong card data
+ *
+ * @return void
+ */
+ public function testFormatPaymentTokenWithWrongData()
+ {
+ unset($this->tokenDetails['type']);
+ $this->paymentTokenMock->expects($this->once())
+ ->method('getTokenDetails')
+ ->willReturn(json_encode($this->tokenDetails));
+ self::expectException('\InvalidArgumentException');
+
+ $this->creditCardTokenFormatter->formatPaymentToken($this->paymentTokenMock);
+ }
+}
diff --git a/app/code/Magento/Braintree/Test/Unit/Model/InstantPurchase/PaymentAdditionalInformationProviderTest.php b/app/code/Magento/Braintree/Test/Unit/Model/InstantPurchase/PaymentAdditionalInformationProviderTest.php
new file mode 100644
index 0000000000000..2631fcbe5f5b5
--- /dev/null
+++ b/app/code/Magento/Braintree/Test/Unit/Model/InstantPurchase/PaymentAdditionalInformationProviderTest.php
@@ -0,0 +1,83 @@
+getPaymentNonceCommandMock = $this->createMock(GetPaymentNonceCommand::class);
+ $this->paymentTokenMock = $this->createMock(PaymentTokenInterface::class);
+ $this->arrayResultMock = $this->createMock(ArrayResult::class);
+ $this->paymentAdditionalInformationProvider = new PaymentAdditionalInformationProvider(
+ $this->getPaymentNonceCommandMock
+ );
+ }
+
+ /**
+ * Test getAdditionalInformation method
+ *
+ * @return void
+ */
+ public function testGetAdditionalInformation()
+ {
+ $customerId = 15;
+ $publicHash = '3n4b7sn48g';
+ $paymentMethodNonce = 'test';
+
+ $this->paymentTokenMock->expects($this->once())->method('getCustomerId')->willReturn($customerId);
+ $this->paymentTokenMock->expects($this->once())->method('getPublicHash')->willReturn($publicHash);
+ $this->getPaymentNonceCommandMock->expects($this->once())->method('execute')->with([
+ PaymentTokenInterface::CUSTOMER_ID => $customerId,
+ PaymentTokenInterface::PUBLIC_HASH => $publicHash,
+ ])->willReturn($this->arrayResultMock);
+ $this->arrayResultMock->expects($this->once())->method('get')
+ ->willReturn(['paymentMethodNonce' => $paymentMethodNonce]);
+
+ $expected = [
+ 'payment_method_nonce' => $paymentMethodNonce,
+ ];
+ $actual = $this->paymentAdditionalInformationProvider->getAdditionalInformation($this->paymentTokenMock);
+ self::assertEquals($expected, $actual);
+ }
+}
diff --git a/app/code/Magento/Braintree/Test/Unit/Model/LocaleResolverTest.php b/app/code/Magento/Braintree/Test/Unit/Model/LocaleResolverTest.php
new file mode 100644
index 0000000000000..b6ef534c55c29
--- /dev/null
+++ b/app/code/Magento/Braintree/Test/Unit/Model/LocaleResolverTest.php
@@ -0,0 +1,138 @@
+configMock = $this->createMock(Config::class);
+ $this->resolverMock = $this->createMock(ResolverInterface::class);
+ $this->localeResolver = new LocaleResolver($this->resolverMock, $this->configMock);
+ }
+
+ /**
+ * Test getDefaultLocalePath method
+ *
+ * @return void
+ */
+ public function testGetDefaultLocalePath()
+ {
+ $expected = 'general/locale/code';
+ $this->resolverMock->expects($this->once())->method('getDefaultLocalePath')->willReturn($expected);
+ $actual = $this->localeResolver->getDefaultLocalePath();
+ self::assertEquals($expected, $actual);
+ }
+
+ /**
+ * Test setDefaultLocale method
+ *
+ * @return void
+ */
+ public function testSetDefaultLocale()
+ {
+ $defaultLocale = 'en_US';
+ $this->resolverMock->expects($this->once())->method('setDefaultLocale')->with($defaultLocale);
+ $this->localeResolver->setDefaultLocale($defaultLocale);
+ }
+
+ /**
+ * Test getDefaultLocale method
+ *
+ * @return void
+ */
+ public function testGetDefaultLocale()
+ {
+ $expected = 'fr_FR';
+ $this->resolverMock->expects($this->once())->method('getDefaultLocale')->willReturn($expected);
+ $actual = $this->localeResolver->getDefaultLocale();
+ self::assertEquals($expected, $actual);
+ }
+
+ /**
+ * Test setLocale method
+ *
+ * @return void
+ */
+ public function testSetLocale()
+ {
+ $locale = 'en_GB';
+ $this->resolverMock->expects($this->once())->method('setLocale')->with($locale);
+ $this->localeResolver->setLocale($locale);
+ }
+
+ /**
+ * Test getLocale method
+ *
+ * @return void
+ */
+ public function testGetLocale()
+ {
+ $locale = 'en_TEST';
+ $allowedLocales = 'en_US,en_GB,en_AU,da_DK,fr_FR,fr_CA,de_DE,zh_HK,it_IT,nl_NL';
+ $this->resolverMock->expects($this->once())->method('getLocale')->willReturn($locale);
+ $this->configMock->expects($this->once())->method('getValue')->with('supported_locales')
+ ->willReturn($allowedLocales);
+
+ $expected = 'en_US';
+ $actual = $this->localeResolver->getLocale();
+ self::assertEquals($expected, $actual);
+ }
+
+ /**
+ * Test emulate method
+ *
+ * @return void
+ */
+ public function testEmulate()
+ {
+ $scopeId = 12;
+ $this->resolverMock->expects($this->once())->method('emulate')->with($scopeId);
+ $this->localeResolver->emulate($scopeId);
+ }
+
+ /**
+ * Test revert method
+ *
+ * @return void
+ */
+ public function testRevert()
+ {
+ $this->resolverMock->expects($this->once())->method('revert');
+ $this->localeResolver->revert();
+ }
+}
diff --git a/app/code/Magento/Catalog/Model/ProductRepository.php b/app/code/Magento/Catalog/Model/ProductRepository.php
index 4c0122694285d..ef2c99c5cb40e 100644
--- a/app/code/Magento/Catalog/Model/ProductRepository.php
+++ b/app/code/Magento/Catalog/Model/ProductRepository.php
@@ -514,13 +514,14 @@ protected function processMediaGallery(ProductInterface $product, $mediaGalleryE
$newEntries = $mediaGalleryEntries;
}
+ $images = (array)$product->getMediaGallery('images');
+ $images = $this->determineImageRoles($product, $images);
+
$this->getMediaGalleryProcessor()->clearMediaAttribute($product, array_keys($product->getMediaAttributes()));
- $images = $product->getMediaGallery('images');
- if ($images) {
- foreach ($images as $image) {
- if (!isset($image['removed']) && !empty($image['types'])) {
- $this->getMediaGalleryProcessor()->setMediaAttribute($product, $image['types'], $image['file']);
- }
+
+ foreach ($images as $image) {
+ if (!isset($image['removed']) && !empty($image['types'])) {
+ $this->getMediaGalleryProcessor()->setMediaAttribute($product, $image['types'], $image['file']);
}
}
@@ -758,6 +759,32 @@ public function cleanCache()
$this->instancesById = null;
}
+ /**
+ * Ascertain image roles, if they are not set against the gallery entries
+ *
+ * @param ProductInterface $product
+ * @param array $images
+ * @return array
+ */
+ private function determineImageRoles(ProductInterface $product, array $images) : array
+ {
+ $imagesWithRoles = [];
+ foreach ($images as $image) {
+ if (!isset($image['types'])) {
+ $image['types'] = [];
+ if (isset($image['file'])) {
+ foreach (array_keys($product->getMediaAttributes()) as $attribute) {
+ if ($image['file'] == $product->getData($attribute)) {
+ $image['types'][] = $attribute;
+ }
+ }
+ }
+ }
+ $imagesWithRoles[] = $image;
+ }
+ return $imagesWithRoles;
+ }
+
/**
* @return Product\Gallery\Processor
*/
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptions.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptions.xml
index 8628ae9b99d3a..6e43753220d7c 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptions.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptions.xml
@@ -16,9 +16,7 @@
-
-
-
+
diff --git a/app/code/Magento/Catalog/Test/Unit/Cron/DeleteAbandonedStoreFlatTablesTest.php b/app/code/Magento/Catalog/Test/Unit/Cron/DeleteAbandonedStoreFlatTablesTest.php
new file mode 100644
index 0000000000000..1a9d7959dda9c
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Unit/Cron/DeleteAbandonedStoreFlatTablesTest.php
@@ -0,0 +1,51 @@
+indexerMock = $this->createMock(Indexer::class);
+ $this->deleteAbandonedStoreFlatTables = new DeleteAbandonedStoreFlatTables($this->indexerMock);
+ }
+
+ /**
+ * Test execute method
+ *
+ * @return void
+ */
+ public function testExecute()
+ {
+ $this->indexerMock->expects($this->once())->method('deleteAbandonedStoreFlatTables');
+ $this->deleteAbandonedStoreFlatTables->execute();
+ }
+}
diff --git a/app/code/Magento/Catalog/Test/Unit/Cron/DeleteOutdatedPriceValuesTest.php b/app/code/Magento/Catalog/Test/Unit/Cron/DeleteOutdatedPriceValuesTest.php
new file mode 100644
index 0000000000000..f1d0034c29580
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Unit/Cron/DeleteOutdatedPriceValuesTest.php
@@ -0,0 +1,139 @@
+resourceConnectionMock = $this->createMock(ResourceConnection::class);
+ $this->attributeRepositoryMock = $this->createMock(AttributeRepository::class);
+ $this->attributeMock = $this->createMock(Attribute::class);
+ $this->scopeConfigMock = $this->createMock(ScopeConfig::class);
+ $this->dbAdapterMock = $this->createMock(AdapterInterface::class);
+ $this->attributeBackendMock = $this->createMock(BackendInterface::class);
+ $this->deleteOutdatedPriceValues = new DeleteOutdatedPriceValues(
+ $this->resourceConnectionMock,
+ $this->attributeRepositoryMock,
+ $this->scopeConfigMock
+ );
+ }
+
+ /**
+ * Test execute method
+ *
+ * @return void
+ */
+ public function testExecute()
+ {
+ $table = 'catalog_product_entity_decimal';
+ $attributeId = 15;
+ $conditions = ['first', 'second'];
+
+ $this->scopeConfigMock
+ ->expects($this->once())
+ ->method('getValue')
+ ->with(Store::XML_PATH_PRICE_SCOPE)
+ ->willReturn(Store::XML_PATH_PRICE_SCOPE);
+ $this->attributeRepositoryMock
+ ->expects($this->once())
+ ->method('get')
+ ->with(ProductAttributeInterface::ENTITY_TYPE_CODE, ProductAttributeInterface::CODE_PRICE)
+ ->willReturn($this->attributeMock);
+ $this->attributeMock->expects($this->once())->method('getId')->willReturn($attributeId);
+ $this->attributeMock->expects($this->once())->method('getBackend')->willReturn($this->attributeBackendMock);
+ $this->attributeBackendMock->expects($this->once())->method('getTable')->willReturn($table);
+ $this->resourceConnectionMock->expects($this->once())
+ ->method('getConnection')
+ ->willReturn($this->dbAdapterMock);
+ $this->dbAdapterMock->expects($this->exactly(2))->method('quoteInto')->willReturnMap([
+ ['attribute_id = ?', $attributeId, null, null, $conditions[0]],
+ ['store_id != ?', Store::DEFAULT_STORE_ID, null, null, $conditions[1]],
+ ]);
+ $this->dbAdapterMock->expects($this->once())->method('delete')->with($table, $conditions);
+ $this->deleteOutdatedPriceValues->execute();
+ }
+
+ /**
+ * Test execute method
+ * The price scope config option is not equal to global value
+ *
+ * @return void
+ */
+ public function testExecutePriceConfigIsNotSetToGlobal()
+ {
+ $this->scopeConfigMock
+ ->expects($this->once())
+ ->method('getValue')
+ ->with(Store::XML_PATH_PRICE_SCOPE)
+ ->willReturn(null);
+ $this->attributeRepositoryMock
+ ->expects($this->never())
+ ->method('get');
+ $this->dbAdapterMock
+ ->expects($this->never())
+ ->method('delete');
+
+ $this->deleteOutdatedPriceValues->execute();
+ }
+}
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 537932a2d4daa..dafea71f872d0 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
@@ -42,7 +42,7 @@
-