diff --git a/app/code/Magento/Backup/Test/Unit/Controller/Adminhtml/Index/RollbackTest.php b/app/code/Magento/Backup/Test/Unit/Controller/Adminhtml/Index/RollbackTest.php new file mode 100644 index 0000000000000..9f306cfc576d8 --- /dev/null +++ b/app/code/Magento/Backup/Test/Unit/Controller/Adminhtml/Index/RollbackTest.php @@ -0,0 +1,288 @@ +objectManagerMock = $this->getMockBuilder(\Magento\Framework\ObjectManagerInterface::class) + ->getMock(); + $this->requestMock = $this->getMockBuilder(\Magento\Framework\App\RequestInterface::class) + ->setMethods(['initForward', 'setDispatched', 'isAjax']) + ->getMockForAbstractClass(); + $this->responseMock = $this->getMockBuilder(\Magento\Framework\App\ResponseInterface::class) + ->setMethods(['setRedirect', 'representJson']) + ->getMockForAbstractClass(); + $this->backupModelFactoryMock = $this->getMockBuilder(\Magento\Backup\Model\BackupFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + $this->backupModelMock = $this->getMockBuilder(\Magento\Backup\Model\Backup::class) + ->disableOriginalConstructor() + ->setMethods(['getTime', 'exists', 'getSize', 'output', 'validateUserPassword']) + ->getMock(); + $this->backupResourceModelMock = $this->getMockBuilder(\Magento\Backup\Model\ResourceModel\Db::class) + ->disableOriginalConstructor() + ->getMock(); + $this->dataHelperMock = $this->getMockBuilder(\Magento\Backup\Helper\Data::class) + ->disableOriginalConstructor() + ->setMethods(['isRollbackAllowed', 'getBackupsDir', 'invalidateCache']) + ->getMock(); + $this->fileFactoryMock = $this->getMockBuilder(\Magento\Framework\App\Response\Http\FileFactory::class) + ->disableOriginalConstructor() + ->getMock(); + $this->resultRedirectFactoryMock = + $this->getMockBuilder(\Magento\Backend\Model\View\Result\RedirectFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + $this->resultRedirectMock = $this->getMockBuilder(\Magento\Backend\Model\View\Result\Redirect::class) + ->disableOriginalConstructor() + ->getMock(); + $this->resultForwardMock = $this->getMockBuilder(\Magento\Backend\Model\View\Result\Forward::class) + ->disableOriginalConstructor() + ->getMock(); + $this->backupFactoryMock = $this->getMockBuilder(\Magento\Framework\Backup\Factory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + $this->backupManagerMock = $this->getMockBuilder(\Magento\Framework\Backup\BackupInterface::class) + ->setMethods(['setName']) + ->getMockForAbstractClass(); + $this->objectManager = new ObjectManager($this); + $this->context = $this->objectManager->getObject( + \Magento\Backend\App\Action\Context::class, + [ + 'objectManager' => $this->objectManagerMock, + 'request' => $this->requestMock, + 'response' => $this->responseMock, + 'resultRedirectFactory' => $this->resultRedirectFactoryMock + ] + ); + $this->rollbackController = $this->objectManager->getObject( + \Magento\Backup\Controller\Adminhtml\Index\Rollback::class, + [ + 'context' => $this->context, + 'backupFactory' => $this->backupFactoryMock, + 'backupModelFactory' => $this->backupModelFactoryMock, + 'fileFactory' => $this->fileFactoryMock + ] + ); + } + + public function testExecuteRollbackDisabled() + { + $rollbackAllowed = false; + + $this->dataHelperMock->expects($this->once()) + ->method('isRollbackAllowed') + ->willReturn($rollbackAllowed); + $this->objectManagerMock->expects($this->once()) + ->method('get') + ->with(\Magento\Backup\Helper\Data::class) + ->willReturn($this->dataHelperMock); + + $this->assertSame($this->responseMock, $this->rollbackController->execute()); + } + + public function testExecuteBackupNotFound() + { + $rollbackAllowed = true; + $isAjax = true; + $time = 0; + $type = 'db'; + $exists = false; + + $this->dataHelperMock->expects($this->once()) + ->method('isRollbackAllowed') + ->willReturn($rollbackAllowed); + $this->objectManagerMock->expects($this->atLeastOnce()) + ->method('get') + ->with(\Magento\Backup\Helper\Data::class) + ->willReturn($this->dataHelperMock); + $this->requestMock->expects($this->once()) + ->method('isAjax') + ->willReturn($isAjax); + $this->backupModelMock->expects($this->atLeastOnce()) + ->method('getTime') + ->willReturn($time); + $this->backupModelMock->expects($this->any()) + ->method('exists') + ->willReturn($exists); + $this->requestMock->expects($this->any()) + ->method('getParam') + ->willReturnMap( + [ + ['time', null, $time], + ['type', null, $type] + ] + ); + $this->backupModelFactoryMock->expects($this->once()) + ->method('create') + ->with($time, $type) + ->willReturn($this->backupModelMock); + + $this->assertSame($this->responseMock, $this->rollbackController->execute()); + } + + public function testExecute() + { + $rollbackAllowed = true; + $isAjax = true; + $time = 1; + $type = 'db'; + $exists = true; + $passwordValid = true; + + $this->dataHelperMock->expects($this->once()) + ->method('isRollbackAllowed') + ->willReturn($rollbackAllowed); + $this->objectManagerMock->expects($this->any()) + ->method('get') + ->with(\Magento\Backup\Helper\Data::class) + ->willReturn($this->dataHelperMock); + $this->requestMock->expects($this->once()) + ->method('isAjax') + ->willReturn($isAjax); + $this->backupModelMock->expects($this->atLeastOnce()) + ->method('getTime') + ->willReturn($time); + $this->backupModelMock->expects($this->any()) + ->method('exists') + ->willReturn($exists); + $this->requestMock->expects($this->any()) + ->method('getParam') + ->willReturnMap( + [ + ['time', null, $time], + ['type', null, $type] + ] + ); + $this->backupModelFactoryMock->expects($this->once()) + ->method('create') + ->with($time, $type) + ->willReturn($this->backupModelMock); + $this->backupManagerMock->expects($this->once()) + ->method('setBackupExtension') + ->willReturn($this->backupManagerMock); + $this->backupManagerMock->expects($this->once()) + ->method('setTime') + ->willReturn($this->backupManagerMock); + $this->backupManagerMock->expects($this->once()) + ->method('setBackupsDir') + ->willReturn($this->backupManagerMock); + $this->backupManagerMock->expects($this->once()) + ->method('setName') + ->willReturn($this->backupManagerMock); + $this->backupManagerMock->expects($this->once()) + ->method('setResourceModel') + ->willReturn($this->backupManagerMock); + $this->backupFactoryMock->expects($this->once()) + ->method('create') + ->with($type) + ->willReturn($this->backupManagerMock); + $this->objectManagerMock->expects($this->at(2)) + ->method('create') + ->with(\Magento\Backup\Model\ResourceModel\Db::class, []) + ->willReturn($this->backupResourceModelMock); + $this->objectManagerMock->expects($this->at(3)) + ->method('create') + ->with(\Magento\Backup\Model\Backup::class, []) + ->willReturn($this->backupModelMock); + $this->backupModelMock->expects($this->once()) + ->method('validateUserPassword') + ->willReturn($passwordValid); + + $this->rollbackController->execute(); + } +} diff --git a/app/code/Magento/Developer/Model/Css/PreProcessor/FileGenerator/PublicationDecorator.php b/app/code/Magento/Developer/Model/Css/PreProcessor/FileGenerator/PublicationDecorator.php index 57af124ba1182..ab7b46ed35d7e 100644 --- a/app/code/Magento/Developer/Model/Css/PreProcessor/FileGenerator/PublicationDecorator.php +++ b/app/code/Magento/Developer/Model/Css/PreProcessor/FileGenerator/PublicationDecorator.php @@ -5,6 +5,9 @@ */ namespace Magento\Developer\Model\Css\PreProcessor\FileGenerator; +use Magento\Framework\App\DeploymentConfig; +use Magento\Framework\App\State; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Filesystem; use Magento\Framework\View\Asset\Repository; use Magento\Framework\App\View\Asset\Publisher; @@ -36,6 +39,11 @@ class PublicationDecorator extends RelatedGenerator */ private $hasRelatedPublishing; + /** + * @var State + */ + private $state; + /** * Constructor * @@ -66,12 +74,27 @@ public function __construct( protected function generateRelatedFile($relatedFileId, LocalInterface $asset) { $relatedAsset = parent::generateRelatedFile($relatedFileId, $asset); - if ($this->hasRelatedPublishing - || WorkflowType::CLIENT_SIDE_COMPILATION === $this->scopeConfig->getValue(WorkflowType::CONFIG_NAME_PATH) - ) { + $isClientSideCompilation = + $this->getState()->getMode() !== State::MODE_PRODUCTION + && WorkflowType::CLIENT_SIDE_COMPILATION === $this->scopeConfig->getValue(WorkflowType::CONFIG_NAME_PATH); + + if ($this->hasRelatedPublishing || $isClientSideCompilation) { $this->assetPublisher->publish($relatedAsset); } return $relatedAsset; } + + /** + * @return State + * @deprecated + */ + private function getState() + { + if (null === $this->state) { + $this->state = ObjectManager::getInstance()->get(State::class); + } + + return $this->state; + } } diff --git a/app/code/Magento/Developer/Model/View/Asset/PreProcessor/PreprocessorStrategy.php b/app/code/Magento/Developer/Model/View/Asset/PreProcessor/PreprocessorStrategy.php index a3051ce443b19..aa40db410a4ce 100644 --- a/app/code/Magento/Developer/Model/View/Asset/PreProcessor/PreprocessorStrategy.php +++ b/app/code/Magento/Developer/Model/View/Asset/PreProcessor/PreprocessorStrategy.php @@ -5,6 +5,9 @@ */ namespace Magento\Developer\Model\View\Asset\PreProcessor; +use Magento\Framework\App\DeploymentConfig; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\App\State; use Magento\Framework\View\Asset\PreProcessor; use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Developer\Model\Config\Source\WorkflowType; @@ -31,6 +34,11 @@ class PreprocessorStrategy implements PreProcessorInterface */ private $scopeConfig; + /** + * @var State + */ + private $state; + /** * Constructor * @@ -56,10 +64,26 @@ public function __construct( */ public function process(PreProcessor\Chain $chain) { - if (WorkflowType::CLIENT_SIDE_COMPILATION === $this->scopeConfig->getValue(WorkflowType::CONFIG_NAME_PATH)) { + $isClientSideCompilation = + $this->getState()->getMode() !== State::MODE_PRODUCTION + && WorkflowType::CLIENT_SIDE_COMPILATION === $this->scopeConfig->getValue(WorkflowType::CONFIG_NAME_PATH); + + if ($isClientSideCompilation) { $this->frontendCompilation->process($chain); } else { $this->alternativeSource->process($chain); } } + + /** + * @return State + * @deprecated + */ + private function getState() + { + if (null === $this->state) { + $this->state = ObjectManager::getInstance()->get(State::class); + } + return $this->state; + } } diff --git a/app/code/Magento/Developer/Model/View/Page/Config/RendererFactory.php b/app/code/Magento/Developer/Model/View/Page/Config/RendererFactory.php index c6e68be05acc9..3301d4b553c63 100644 --- a/app/code/Magento/Developer/Model/View/Page/Config/RendererFactory.php +++ b/app/code/Magento/Developer/Model/View/Page/Config/RendererFactory.php @@ -6,6 +6,8 @@ namespace Magento\Developer\Model\View\Page\Config; use Magento\Developer\Model\Config\Source\WorkflowType; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\App\State; use Magento\Store\Model\ScopeInterface; /** @@ -58,7 +60,9 @@ public function __construct( */ public function create(array $data = []) { - $renderer = $this->scopeConfig->getValue(WorkflowType::CONFIG_NAME_PATH, ScopeInterface::SCOPE_STORE); + $renderer = $this->objectManager->get(State::class)->getMode() === State::MODE_PRODUCTION ? + WorkflowType::SERVER_SIDE_COMPILATION : + $this->scopeConfig->getValue(WorkflowType::CONFIG_NAME_PATH, ScopeInterface::SCOPE_STORE); return $this->objectManager->create( $this->rendererTypes[$renderer], diff --git a/app/code/Magento/Developer/Test/Unit/Model/Css/PreProcessor/FileGenerator/PublicationDecoratorTest.php b/app/code/Magento/Developer/Test/Unit/Model/Css/PreProcessor/FileGenerator/PublicationDecoratorTest.php index cddab4d253acc..8233437cf927f 100644 --- a/app/code/Magento/Developer/Test/Unit/Model/Css/PreProcessor/FileGenerator/PublicationDecoratorTest.php +++ b/app/code/Magento/Developer/Test/Unit/Model/Css/PreProcessor/FileGenerator/PublicationDecoratorTest.php @@ -5,71 +5,131 @@ */ namespace Magento\Developer\Test\Unit\Model\Css\PreProcessor\FileGenerator; +use Magento\Framework\App\State; +use Magento\Framework\App\View\Asset\Publisher; +use Magento\Framework\Css\PreProcessor\Instruction\Import; use Magento\Framework\Filesystem; use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\Css\PreProcessor\File\Temporary; use Magento\Developer\Model\Css\PreProcessor\FileGenerator\PublicationDecorator; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Framework\View\Asset\File; +use Magento\Framework\View\Asset\LocalInterface; +use Magento\Framework\View\Asset\Repository; /** - * Class PublicationDecoratorTest + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class PublicationDecoratorTest extends \PHPUnit_Framework_TestCase { /** - * Calls generate method to access protected method generateRelatedFile + * @var PublicationDecorator */ - public function testGenerateRelatedFile() + private $model; + + /** + * @var Filesystem|\PHPUnit_Framework_MockObject_MockObject + */ + private $filesystemMock; + + /** + * @var Temporary|\PHPUnit_Framework_MockObject_MockObject + */ + private $temporaryFileMock; + + /** + * @var Publisher|\PHPUnit_Framework_MockObject_MockObject + */ + private $publisherMock; + + /** + * @var Repository|\PHPUnit_Framework_MockObject_MockObject + */ + private $assetRepositoryMock; + + /** + * @var File|\PHPUnit_Framework_MockObject_MockObject + */ + private $relatedAssetMock; + + /** + * @var Import|\PHPUnit_Framework_MockObject_MockObject + */ + private $importGeneratorMock; + + /** + * @var LocalInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $localAssetMock; + + /** + * @var ScopeConfigInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $scopeConfigMock; + + /** + * @var State|\PHPUnit_Framework_MockObject_MockObject + */ + private $stateMock; + + protected function setUp() { - $filesystemMock = $this->getMockBuilder(Filesystem::class) + $this->filesystemMock = $this->getMockBuilder(Filesystem::class) ->disableOriginalConstructor() ->getMock(); - $fileTemporaryMock = $this->getMockBuilder(Temporary::class) + $this->temporaryFileMock = $this->getMockBuilder(Temporary::class) ->disableOriginalConstructor() ->getMock(); - - $publisherMock = $this->getMockBuilder(\Magento\Framework\App\View\Asset\Publisher::class) + $this->publisherMock = $this->getMockBuilder(Publisher::class) ->disableOriginalConstructor() ->getMock(); - $assetRepoMock = $this->getMockBuilder(\Magento\Framework\View\Asset\Repository::class) + $this->assetRepositoryMock = $this->getMockBuilder(Repository::class) ->disableOriginalConstructor() ->getMock(); - $relatedAssetMock = $this->getMockBuilder(\Magento\Framework\View\Asset\File::class) + $this->relatedAssetMock = $this->getMockBuilder(File::class) ->disableOriginalConstructor() ->getMock(); - $importGeneratorMock = $this->getMockBuilder(\Magento\Framework\Css\PreProcessor\Instruction\Import::class) + $this->importGeneratorMock = $this->getMockBuilder(Import::class) ->disableOriginalConstructor() ->getMock(); - $localAssetMock = $this->getMockBuilder(\Magento\Framework\View\Asset\LocalInterface::class) + $this->localAssetMock = $this->getMockBuilder(LocalInterface::class) ->disableOriginalConstructor() ->getMock(); - $scopeConfigMock = $this->getMockBuilder(ScopeConfigInterface::class) + $this->scopeConfigMock = $this->getMockBuilder(ScopeConfigInterface::class) ->getMockForAbstractClass(); + $this->stateMock = $this->getMockBuilder(State::class) + ->disableOriginalConstructor() + ->getMock(); - $relatedFileId = 'file_id'; + $this->model = (new ObjectManager($this))->getObject(PublicationDecorator::class, [ + 'filesystem' => $this->filesystemMock, + 'assetRepository' => $this->assetRepositoryMock, + 'temporaryFile' => $this->temporaryFileMock, + 'assetPublisher' => $this->publisherMock, + 'scopeConfig' => $this->scopeConfigMock, + 'state' => $this->stateMock, + 'hasRelatedPublishing' => true + ]); + } - $relatedFiles = [[$relatedFileId, $localAssetMock]]; + /** + * Calls generate method to access protected method generateRelatedFile + */ + public function testGenerateRelatedFile() + { + $relatedFileId = 'file_id'; + $relatedFiles = [[$relatedFileId, $this->localAssetMock]]; - $importGeneratorMock->expects(self::any()) + $this->importGeneratorMock->expects($this->any()) ->method('getRelatedFiles') - ->will(self::onConsecutiveCalls($relatedFiles, [])); - - $assetRepoMock->expects(self::any()) + ->willReturnOnConsecutiveCalls($relatedFiles, []); + $this->assetRepositoryMock->expects($this->any()) ->method('createRelated') - ->willReturn($relatedAssetMock); - - $publisherMock->expects(self::once()) + ->willReturn($this->relatedAssetMock); + $this->publisherMock->expects($this->once()) ->method('publish') - ->with($relatedAssetMock); - - $model = new PublicationDecorator( - $filesystemMock, - $assetRepoMock, - $fileTemporaryMock, - $publisherMock, - $scopeConfigMock, - true - ); - - $model->generate($importGeneratorMock); + ->with($this->relatedAssetMock); + + $this->model->generate($this->importGeneratorMock); } } diff --git a/app/code/Magento/Developer/Test/Unit/Model/View/Asset/PreProcessor/PreprocessorStrategyTest.php b/app/code/Magento/Developer/Test/Unit/Model/View/Asset/PreProcessor/PreprocessorStrategyTest.php index 25fbe112e7dc8..d27fa0642d061 100644 --- a/app/code/Magento/Developer/Test/Unit/Model/View/Asset/PreProcessor/PreprocessorStrategyTest.php +++ b/app/code/Magento/Developer/Test/Unit/Model/View/Asset/PreProcessor/PreprocessorStrategyTest.php @@ -5,6 +5,9 @@ */ namespace Magento\Developer\Test\Unit\Model\View\Asset\PreProcessor; +use Magento\Framework\App\DeploymentConfig; +use Magento\Framework\App\State; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Framework\View\Asset\PreProcessor\Chain; use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Developer\Model\Config\Source\WorkflowType; @@ -16,6 +19,7 @@ * Class PreprocessorStrategyTest * * @see \Magento\Developer\Model\View\Asset\PreProcessor\PreprocessorStrategy + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class PreprocessorStrategyTest extends \PHPUnit_Framework_TestCase { @@ -39,6 +43,16 @@ class PreprocessorStrategyTest extends \PHPUnit_Framework_TestCase */ private $scopeConfigMock; + /** + * @var State|\PHPUnit_Framework_MockObject_MockObject + */ + private $stateMock; + + /** + * @var \Magento\Framework\App\ObjectManager|\PHPUnit_Framework_MockObject_MockObject + */ + private $objectMangerMock; + /** * Set up */ @@ -51,12 +65,19 @@ protected function setUp() ->getMock(); $this->scopeConfigMock = $this->getMockBuilder(ScopeConfigInterface::class) ->getMockForAbstractClass(); + $this->stateMock = $this->getMockBuilder(State::class) + ->disableOriginalConstructor() + ->getMock(); + $this->objectMangerMock = $this->getMockBuilder(\Magento\Framework\App\ObjectManager::class) + ->disableOriginalConstructor() + ->getMock(); - $this->preprocessorStrategy = new PreprocessorStrategy( - $this->alternativeSourceMock, - $this->frontendCompilationMock, - $this->scopeConfigMock - ); + $this->preprocessorStrategy = (new ObjectManager($this))->getObject(PreprocessorStrategy::class, [ + 'alternativeSource' => $this->alternativeSourceMock, + 'frontendCompilation' => $this->frontendCompilationMock, + 'scopeConfig' => $this->scopeConfigMock, + 'state' => $this->stateMock, + ]); } /** @@ -70,13 +91,36 @@ public function testProcessClientSideCompilation() ->method('getValue') ->with(WorkflowType::CONFIG_NAME_PATH) ->willReturn(WorkflowType::CLIENT_SIDE_COMPILATION); - $this->frontendCompilationMock->expects(self::once()) ->method('process') ->with($chainMock); + $this->alternativeSourceMock->expects(self::never()) + ->method('process'); + $this->stateMock->expects($this->atLeastOnce()) + ->method('getMode') + ->willReturn(State::MODE_DEVELOPER); + + $this->preprocessorStrategy->process($chainMock); + } + + public function testProcessClientSideCompilationWithDefaultMode() + { + $chainMock = $this->getChainMock(); + $this->scopeConfigMock->expects(self::once()) + ->method('getValue') + ->with(WorkflowType::CONFIG_NAME_PATH) + ->willReturn(WorkflowType::CLIENT_SIDE_COMPILATION); + $this->frontendCompilationMock->expects(self::once()) + ->method('process') + ->with($chainMock); $this->alternativeSourceMock->expects(self::never()) ->method('process'); + $this->stateMock->expects($this->once()) + ->method('getMode') + ->willReturn(State::MODE_DEFAULT); + + \Magento\Framework\App\ObjectManager::setInstance($this->objectMangerMock); $this->preprocessorStrategy->process($chainMock); } @@ -88,17 +132,18 @@ public function testProcessAlternativeSource() { $chainMock = $this->getChainMock(); - $this->scopeConfigMock->expects(self::once()) + $this->scopeConfigMock->expects($this->never()) ->method('getValue') ->with(WorkflowType::CONFIG_NAME_PATH) ->willReturn('off'); - $this->alternativeSourceMock->expects(self::once()) ->method('process') ->with($chainMock); - $this->frontendCompilationMock->expects(self::never()) ->method('process'); + $this->stateMock->expects($this->atLeastOnce()) + ->method('getMode') + ->willReturn(State::MODE_PRODUCTION); $this->preprocessorStrategy->process($chainMock); } diff --git a/app/code/Magento/Developer/Test/Unit/Model/View/Page/Config/RendererFactoryTest.php b/app/code/Magento/Developer/Test/Unit/Model/View/Page/Config/RendererFactoryTest.php index 06ebf3208b017..94305fca9f9aa 100644 --- a/app/code/Magento/Developer/Test/Unit/Model/View/Page/Config/RendererFactoryTest.php +++ b/app/code/Magento/Developer/Test/Unit/Model/View/Page/Config/RendererFactoryTest.php @@ -6,51 +6,82 @@ namespace Magento\Developer\Test\Unit\Model\View\Page\Config; use Magento\Developer\Model\Config\Source\WorkflowType; +use Magento\Developer\Model\View\Page\Config\RendererFactory; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\State; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\View\Page\Config\RendererInterface; use Magento\Store\Model\ScopeInterface; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +/** + * Class RendererFactoryTest + */ class RendererFactoryTest extends \PHPUnit_Framework_TestCase { - const RENDERER_TYPE = 'renderer_type'; + /** + * @var ObjectManagerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $objectManagerMock; - const RENDERER_INSTANCE_NAME = 'renderer'; + /** + * @var RendererInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $rendererMock; - public function testCreate() + /** + * @var ScopeConfigInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $configMock; + + /** + * @var State|\PHPUnit_Framework_MockObject_MockObject + */ + private $stateMock; + + /** + * @var RendererFactory + */ + private $model; + + protected function setUp() { - // Set up mocks - $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); - $objectManagerMock = $this->getMockBuilder(\Magento\Framework\ObjectManagerInterface::class) + $this->objectManagerMock = $this->getMockBuilder(ObjectManagerInterface::class) + ->getMockForAbstractClass(); + $this->configMock = $this->getMockBuilder(ScopeConfigInterface::class) + ->getMockForAbstractClass(); + $this->rendererMock = $this->getMockBuilder(RendererInterface::class) ->disableOriginalConstructor() ->getMock(); - $configMock = $this->getMockBuilder(\Magento\Framework\App\Config\ScopeConfigInterface::class) + $this->stateMock = $this->getMockBuilder(State::class) ->disableOriginalConstructor() ->getMock(); - $rendererMock = $this->getMockBuilder(\Magento\Framework\View\Page\Config\RendererInterface::class) - ->disableOriginalConstructor() - ->getMock(); - $createArgs = [1,2,3]; - $objectManagerMock->expects($this->once()) + $this->objectManagerMock->expects($this->any()) + ->method('get') + ->with(State::class) + ->willReturn($this->stateMock); + + $this->model = (new ObjectManagerHelper($this))->getObject(RendererFactory::class, [ + 'objectManager' => $this->objectManagerMock, + 'scopeConfig' => $this->configMock, + 'rendererTypes' => ['renderer_type' => 'renderer'], + ]); + } + + public function testCreate() + { + $this->stateMock->expects($this->once()) + ->method('getMode'); + $this->objectManagerMock->expects($this->once()) ->method('create') - ->with(self::RENDERER_INSTANCE_NAME, $createArgs) - ->willReturn($rendererMock); - $configMock->expects($this->once()) + ->with('renderer', []) + ->willReturn($this->rendererMock); + $this->configMock->expects($this->once()) ->method('getValue') ->with(WorkflowType::CONFIG_NAME_PATH, ScopeInterface::SCOPE_STORE) - ->willReturn(self::RENDERER_TYPE); - - // Set up System Under Test - $rendererTypes = [ - self::RENDERER_TYPE => self::RENDERER_INSTANCE_NAME - ]; - $sutArgs = [ - 'objectManager' => $objectManagerMock, - 'scopeConfig' => $configMock, - 'rendererTypes' => $rendererTypes - ]; - - $model = $objectManager->getObject(\Magento\Developer\Model\View\Page\Config\RendererFactory::class, $sutArgs); - - // Test - $this->assertSame($rendererMock, $model->create($createArgs)); + ->willReturn('renderer_type'); + + $this->assertSame($this->rendererMock, $this->model->create()); } } diff --git a/app/code/Magento/Sales/Api/Data/CommentInterface.php b/app/code/Magento/Sales/Api/Data/CommentInterface.php new file mode 100644 index 0000000000000..d7021dc9f9546 --- /dev/null +++ b/app/code/Magento/Sales/Api/Data/CommentInterface.php @@ -0,0 +1,44 @@ +extensionAttributes; + } + + /** + * {@inheritdoc} + */ + public function setExtensionAttributes( + \Magento\Sales\Api\Data\InvoiceCreationArgumentsExtensionInterface $extensionAttributes + ) { + $this->extensionAttributes = $extensionAttributes; + + return $this; + } +} diff --git a/app/code/Magento/Sales/Model/Order/Invoice/ItemCreation.php b/app/code/Magento/Sales/Model/Order/Invoice/ItemCreation.php new file mode 100644 index 0000000000000..abc19c3aaa73d --- /dev/null +++ b/app/code/Magento/Sales/Model/Order/Invoice/ItemCreation.php @@ -0,0 +1,57 @@ +orderItemId; + } + + /** + * {@inheritdoc} + */ + public function setOrderItemId($orderItemId) + { + $this->orderItemId = $orderItemId; + } + + /** + * {@inheritdoc} + */ + public function getQty() + { + return $this->qty; + } + + /** + * {@inheritdoc} + */ + public function setQty($qty) + { + $this->qty = $qty; + } +} diff --git a/app/code/Magento/Sales/Model/Order/Invoice/Notifier.php b/app/code/Magento/Sales/Model/Order/Invoice/Notifier.php new file mode 100644 index 0000000000000..93755b2df176f --- /dev/null +++ b/app/code/Magento/Sales/Model/Order/Invoice/Notifier.php @@ -0,0 +1,41 @@ +senders = $senders; + } + + /** + * {@inheritdoc} + */ + public function notify( + \Magento\Sales\Api\Data\OrderInterface $order, + \Magento\Sales\Api\Data\InvoiceInterface $invoice, + \Magento\Sales\Api\Data\InvoiceCommentCreationInterface $comment = null, + $forceSyncMode = false + ) { + foreach ($this->senders as $sender) { + $sender->send($order, $invoice, $comment, $forceSyncMode); + } + } +} diff --git a/app/code/Magento/Sales/Model/Order/Invoice/NotifierInterface.php b/app/code/Magento/Sales/Model/Order/Invoice/NotifierInterface.php new file mode 100644 index 0000000000000..76f9add8c2df7 --- /dev/null +++ b/app/code/Magento/Sales/Model/Order/Invoice/NotifierInterface.php @@ -0,0 +1,32 @@ +eventManager = $context->getEventDispatcher(); + } + + /** + * @param \Magento\Sales\Api\Data\OrderInterface $order + * @param \Magento\Sales\Api\Data\InvoiceInterface $invoice + * @param bool $capture + * + * @return \Magento\Sales\Api\Data\OrderInterface + */ + public function execute( + \Magento\Sales\Api\Data\OrderInterface $order, + \Magento\Sales\Api\Data\InvoiceInterface $invoice, + $capture + ) { + $this->calculateOrderItemsTotals( + $invoice->getItems() + ); + + if ($this->canCapture($order, $invoice)) { + if ($capture) { + $invoice->capture(); + } else { + $invoice->setCanVoidFlag(false); + + $invoice->pay(); + } + } elseif (!$order->getPayment()->getMethodInstance()->isGateway() || !$capture) { + if (!$order->getPayment()->getIsTransactionPending()) { + $invoice->setCanVoidFlag(false); + + $invoice->pay(); + } + } + + $this->calculateOrderTotals($order, $invoice); + + if (null === $invoice->getState()) { + $invoice->setState(\Magento\Sales\Model\Order\Invoice::STATE_OPEN); + } + + $this->eventManager->dispatch( + 'sales_order_invoice_register', + ['invoice' => $invoice, 'order' => $order] + ); + + return $order; + } + + /** + * Calculates totals of Order Items according to given Invoice Items. + * + * @param \Magento\Sales\Api\Data\InvoiceItemInterface[] $items + * + * @return void + */ + private function calculateOrderItemsTotals($items) + { + foreach ($items as $item) { + if ($item->isDeleted()) { + continue; + } + + if ($item->getQty() > 0) { + $item->register(); + } else { + $item->isDeleted(true); + } + } + } + + /** + * Checks Invoice capture action availability. + * + * @param \Magento\Sales\Api\Data\OrderInterface $order + * @param \Magento\Sales\Api\Data\InvoiceInterface $invoice + * + * @return bool + */ + private function canCapture( + \Magento\Sales\Api\Data\OrderInterface $order, + \Magento\Sales\Api\Data\InvoiceInterface $invoice + ) { + return $invoice->getState() != \Magento\Sales\Model\Order\Invoice::STATE_CANCELED && + $invoice->getState() != \Magento\Sales\Model\Order\Invoice::STATE_PAID && + $order->getPayment()->canCapture(); + } + + /** + * Calculates Order totals according to given Invoice. + * + * @param \Magento\Sales\Api\Data\OrderInterface $order + * @param \Magento\Sales\Api\Data\InvoiceInterface $invoice + * + * @return void + */ + private function calculateOrderTotals( + \Magento\Sales\Api\Data\OrderInterface $order, + \Magento\Sales\Api\Data\InvoiceInterface $invoice + ) { + $order->setTotalInvoiced( + $order->getTotalInvoiced() + $invoice->getGrandTotal() + ); + $order->setBaseTotalInvoiced( + $order->getBaseTotalInvoiced() + $invoice->getBaseGrandTotal() + ); + + $order->setSubtotalInvoiced( + $order->getSubtotalInvoiced() + $invoice->getSubtotal() + ); + $order->setBaseSubtotalInvoiced( + $order->getBaseSubtotalInvoiced() + $invoice->getBaseSubtotal() + ); + + $order->setTaxInvoiced( + $order->getTaxInvoiced() + $invoice->getTaxAmount() + ); + $order->setBaseTaxInvoiced( + $order->getBaseTaxInvoiced() + $invoice->getBaseTaxAmount() + ); + + $order->setDiscountTaxCompensationInvoiced( + $order->getDiscountTaxCompensationInvoiced() + $invoice->getDiscountTaxCompensationAmount() + ); + $order->setBaseDiscountTaxCompensationInvoiced( + $order->getBaseDiscountTaxCompensationInvoiced() + $invoice->getBaseDiscountTaxCompensationAmount() + ); + + $order->setShippingTaxInvoiced( + $order->getShippingTaxInvoiced() + $invoice->getShippingTaxAmount() + ); + $order->setBaseShippingTaxInvoiced( + $order->getBaseShippingTaxInvoiced() + $invoice->getBaseShippingTaxAmount() + ); + + $order->setShippingInvoiced( + $order->getShippingInvoiced() + $invoice->getShippingAmount() + ); + $order->setBaseShippingInvoiced( + $order->getBaseShippingInvoiced() + $invoice->getBaseShippingAmount() + ); + + $order->setDiscountInvoiced( + $order->getDiscountInvoiced() + $invoice->getDiscountAmount() + ); + $order->setBaseDiscountInvoiced( + $order->getBaseDiscountInvoiced() + $invoice->getBaseDiscountAmount() + ); + + $order->setBaseTotalInvoicedCost( + $order->getBaseTotalInvoicedCost() + $invoice->getBaseCost() + ); + } +} diff --git a/app/code/Magento/Sales/Model/Order/Invoice/Sender/EmailSender.php b/app/code/Magento/Sales/Model/Order/Invoice/Sender/EmailSender.php new file mode 100644 index 0000000000000..cecdd2702c939 --- /dev/null +++ b/app/code/Magento/Sales/Model/Order/Invoice/Sender/EmailSender.php @@ -0,0 +1,149 @@ +paymentHelper = $paymentHelper; + $this->invoiceResource = $invoiceResource; + $this->globalConfig = $globalConfig; + $this->eventManager = $eventManager; + } + + /** + * Sends order invoice email to the customer. + * + * Email will be sent immediately in two cases: + * + * - if asynchronous email sending is disabled in global settings + * - if $forceSyncMode parameter is set to TRUE + * + * Otherwise, email will be sent later during running of + * corresponding cron job. + * + * @param \Magento\Sales\Api\Data\OrderInterface $order + * @param \Magento\Sales\Api\Data\InvoiceInterface $invoice + * @param \Magento\Sales\Api\Data\InvoiceCommentCreationInterface|null $comment + * @param bool $forceSyncMode + * + * @return bool + */ + public function send( + \Magento\Sales\Api\Data\OrderInterface $order, + \Magento\Sales\Api\Data\InvoiceInterface $invoice, + \Magento\Sales\Api\Data\InvoiceCommentCreationInterface $comment = null, + $forceSyncMode = false + ) { + $invoice->setSendEmail(true); + + if (!$this->globalConfig->getValue('sales_email/general/async_sending') || $forceSyncMode) { + $transport = [ + 'order' => $order, + 'invoice' => $invoice, + 'comment' => $comment ? $comment->getComment() : '', + 'billing' => $order->getBillingAddress(), + 'payment_html' => $this->getPaymentHtml($order), + 'store' => $order->getStore(), + 'formattedShippingAddress' => $this->getFormattedShippingAddress($order), + 'formattedBillingAddress' => $this->getFormattedBillingAddress($order) + ]; + + $this->eventManager->dispatch( + 'email_invoice_set_template_vars_before', + ['sender' => $this, 'transport' => $transport] + ); + + $this->templateContainer->setTemplateVars($transport); + + if ($this->checkAndSend($order)) { + $invoice->setEmailSent(true); + + $this->invoiceResource->saveAttribute($invoice, ['send_email', 'email_sent']); + + return true; + } + } else { + $invoice->setEmailSent(null); + + $this->invoiceResource->saveAttribute($invoice, 'email_sent'); + } + + $this->invoiceResource->saveAttribute($invoice, 'send_email'); + + return false; + } + + /** + * Returns payment info block as HTML. + * + * @param \Magento\Sales\Api\Data\OrderInterface $order + * + * @return string + */ + private function getPaymentHtml(\Magento\Sales\Api\Data\OrderInterface $order) + { + return $this->paymentHelper->getInfoBlockHtml( + $order->getPayment(), + $this->identityContainer->getStore()->getStoreId() + ); + } +} diff --git a/app/code/Magento/Sales/Model/Order/Invoice/SenderInterface.php b/app/code/Magento/Sales/Model/Order/Invoice/SenderInterface.php new file mode 100644 index 0000000000000..30f677018eb1a --- /dev/null +++ b/app/code/Magento/Sales/Model/Order/Invoice/SenderInterface.php @@ -0,0 +1,29 @@ +invoiceService = $invoiceService; + } + + /** + * @param OrderInterface $order + * @param array $items + * @param InvoiceCommentCreationInterface|null $comment + * @param bool|false $appendComment + * @param InvoiceCreationArgumentsInterface|null $arguments + * @return InvoiceInterface + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function create( + OrderInterface $order, + $items = [], + InvoiceCommentCreationInterface $comment = null, + $appendComment = false, + InvoiceCreationArgumentsInterface $arguments = null + ) { + + $invoiceItems = $this->itemsToArray($items); + $invoice = $this->invoiceService->prepareInvoice($order, $invoiceItems); + + if ($comment) { + $invoice->addComment( + $comment->getComment(), + $appendComment, + $comment->getIsVisibleOnFront() + ); + } + + return $invoice; + } + + /** + * Convert Items To Array + * + * @param InvoiceItemCreationInterface[] $items + * @return array + */ + private function itemsToArray($items = []) + { + $invoiceItems = []; + foreach ($items as $item) { + $invoiceItems[$item->getOrderItemId()] = $item->getQty(); + } + return $invoiceItems; + } +} diff --git a/app/code/Magento/Sales/Model/Order/InvoiceNotifierInterface.php b/app/code/Magento/Sales/Model/Order/InvoiceNotifierInterface.php new file mode 100644 index 0000000000000..f055bbd4f8cfd --- /dev/null +++ b/app/code/Magento/Sales/Model/Order/InvoiceNotifierInterface.php @@ -0,0 +1,31 @@ +orderValidator = $orderValidator; + } + + /** + * @param InvoiceInterface $invoice + * @param OrderInterface $order + * @return array + */ + public function validate(InvoiceInterface $invoice, OrderInterface $order) + { + $messages = $this->checkQtyAvailability($invoice, $order); + + if (!$this->orderValidator->canInvoice($order)) { + $messages[] = __( + 'An invoice cannot be created when an order has a status of %1.', + $order->getStatus() + ); + } + return $messages; + } + + /** + * Check qty availability + * + * @param InvoiceInterface $invoice + * @param OrderInterface $order + * @return array + */ + private function checkQtyAvailability(InvoiceInterface $invoice, OrderInterface $order) + { + $messages = []; + $qtys = $this->getInvoiceQty($invoice); + + $totalQty = 0; + if ($qtys) { + /** @var \Magento\Sales\Model\Order\Item $orderItem */ + foreach ($order->getItems() as $orderItem) { + if (isset($qtys[$orderItem->getId()])) { + if ($qtys[$orderItem->getId()] > $orderItem->getQtyToInvoice() && !$orderItem->isDummy()) { + $messages[] = __( + 'The quantity to invoice must not be greater than the uninvoiced quantity' + . ' for product SKU "%1".', + $orderItem->getSku() + ); + } + $totalQty += $qtys[$orderItem->getId()]; + unset($qtys[$orderItem->getId()]); + } + } + } + if ($qtys) { + $messages[] = __('The invoice contains one or more items that are not part of the original order.'); + } elseif ($totalQty <= 0) { + $messages[] = __('You can\'t create an invoice without products.'); + } + return $messages; + } + + /** + * @param InvoiceInterface $invoice + * @return array + */ + private function getInvoiceQty(InvoiceInterface $invoice) + { + $qtys = []; + /** @var InvoiceItemInterface $item */ + foreach ($invoice->getItems() as $item) { + $qtys[$item->getOrderItemId()] = $item->getQty(); + } + return $qtys; + } +} diff --git a/app/code/Magento/Sales/Model/Order/InvoiceValidatorInterface.php b/app/code/Magento/Sales/Model/Order/InvoiceValidatorInterface.php new file mode 100644 index 0000000000000..64b2f98dfe37e --- /dev/null +++ b/app/code/Magento/Sales/Model/Order/InvoiceValidatorInterface.php @@ -0,0 +1,25 @@ +getState() === Order::STATE_PAYMENT_REVIEW || + $order->getState() === Order::STATE_HOLDED || + $order->getState() === Order::STATE_CANCELED || + $order->getState() === Order::STATE_COMPLETE || + $order->getState() === Order::STATE_CLOSED + ) { + return false; + }; + /** @var \Magento\Sales\Model\Order\Item $item */ + foreach ($order->getItems() as $item) { + if ($item->getQtyToInvoice() > 0 && !$item->getLockedDoInvoice()) { + return true; + } + } + return false; + } +} diff --git a/app/code/Magento/Sales/Model/Order/OrderValidatorInterface.php b/app/code/Magento/Sales/Model/Order/OrderValidatorInterface.php new file mode 100644 index 0000000000000..d0dcc38af642a --- /dev/null +++ b/app/code/Magento/Sales/Model/Order/OrderValidatorInterface.php @@ -0,0 +1,23 @@ +payOperation = $payOperation; + } + + /** + * {@inheritdoc} + */ + public function pay( + \Magento\Sales\Api\Data\OrderInterface $order, + \Magento\Sales\Api\Data\InvoiceInterface $invoice, + $capture + ) { + return $this->payOperation->execute($order, $invoice, $capture); + } +} diff --git a/app/code/Magento/Sales/Model/Order/PaymentAdapterInterface.php b/app/code/Magento/Sales/Model/Order/PaymentAdapterInterface.php new file mode 100644 index 0000000000000..0e4b193169da8 --- /dev/null +++ b/app/code/Magento/Sales/Model/Order/PaymentAdapterInterface.php @@ -0,0 +1,26 @@ +getBaseGrandTotal() || $order->canCreditmemo()) { + return true; + } + return false; + } + + /** + * Check if order should be in closed state + * + * @param OrderInterface $order + * @param array $arguments + * @return bool + */ + private function isOrderClosed(OrderInterface $order, $arguments) + { + /** @var $order Order|OrderInterface */ + $forceCreditmemo = in_array(self::FORCED_CREDITMEMO, $arguments); + if (floatval($order->getTotalRefunded()) || !$order->getTotalRefunded() && $forceCreditmemo) { + return true; + } + return false; + } + + /** + * Check if order is processing + * + * @param OrderInterface $order + * @param array $arguments + * @return bool + */ + private function isOrderProcessing(OrderInterface $order, $arguments) + { + /** @var $order Order|OrderInterface */ + if ($order->getState() == Order::STATE_NEW && in_array(self::IN_PROGRESS, $arguments)) { + return true; + } + return false; + } + + /** + * Returns initial state for order + * + * @param OrderInterface $order + * @return string + */ + private function getInitialOrderState(OrderInterface $order) + { + return $order->getState() === Order::STATE_PROCESSING ? Order::STATE_PROCESSING : Order::STATE_NEW; + } + + /** + * @param OrderInterface $order + * @param array $arguments + * @return string + */ + public function getStateForOrder(OrderInterface $order, array $arguments = []) + { + /** @var $order Order|OrderInterface */ + $orderState = $this->getInitialOrderState($order); + if (!$order->isCanceled() && !$order->canUnhold() && !$order->canInvoice() && !$order->canShip()) { + if ($this->isOrderComplete($order)) { + $orderState = Order::STATE_COMPLETE; + } elseif ($this->isOrderClosed($order, $arguments)) { + $orderState = Order::STATE_CLOSED; + } + } + if ($this->isOrderProcessing($order, $arguments)) { + $orderState = Order::STATE_PROCESSING; + } + return $orderState; + } +} diff --git a/app/code/Magento/Sales/Model/OrderInvoice.php b/app/code/Magento/Sales/Model/OrderInvoice.php new file mode 100644 index 0000000000000..4a343e559d877 --- /dev/null +++ b/app/code/Magento/Sales/Model/OrderInvoice.php @@ -0,0 +1,182 @@ +resourceConnection = $resourceConnection; + $this->orderRepository = $orderRepository; + $this->invoiceDocumentFactory = $invoiceDocumentFactory; + $this->invoiceValidator = $invoiceValidator; + $this->paymentAdapter = $paymentAdapter; + $this->orderStateResolver = $orderStateResolver; + $this->config = $config; + $this->invoiceRepository = $invoiceRepository; + $this->notifierInterface = $notifierInterface; + $this->logger = $logger; + } + + /** + * @param int $orderId + * @param bool $capture + * @param array $items + * @param bool $notify + * @param bool $appendComment + * @param \Magento\Sales\Api\Data\InvoiceCommentCreationInterface|null $comment + * @param \Magento\Sales\Api\Data\InvoiceCreationArgumentsInterface|null $arguments + * @return int + * @throws \Magento\Sales\Api\Exception\DocumentValidationExceptionInterface + * @throws \Magento\Sales\Api\Exception\CouldNotInvoiceExceptionInterface + * @throws \Magento\Framework\Exception\InputException + * @throws \Magento\Framework\Exception\NoSuchEntityException + * @throws \DomainException + */ + public function execute( + $orderId, + $capture = false, + array $items = [], + $notify = false, + $appendComment = false, + InvoiceCommentCreationInterface $comment = null, + InvoiceCreationArgumentsInterface $arguments = null + ) { + $connection = $this->resourceConnection->getConnection('sales'); + $order = $this->orderRepository->get($orderId); + $invoice = $this->invoiceDocumentFactory->create( + $order, + $items, + $comment, + ($appendComment && $notify), + $arguments + ); + $errorMessages = $this->invoiceValidator->validate($invoice, $order); + if (!empty($errorMessages)) { + throw new \Magento\Sales\Exception\DocumentValidationException( + __("Invoice Document Validation Error(s):\n" . implode("\n", $errorMessages)) + ); + } + $connection->beginTransaction(); + try { + $order = $this->paymentAdapter->pay($order, $invoice, $capture); + $order->setState( + $this->orderStateResolver->getStateForOrder($order, [OrderStateResolverInterface::IN_PROGRESS]) + ); + $order->setStatus($this->config->getStateDefaultStatus($order->getState())); + $invoice->setState(\Magento\Sales\Model\Order\Invoice::STATE_PAID); + $this->invoiceRepository->save($invoice); + $this->orderRepository->save($order); + $connection->commit(); + } catch (\Exception $e) { + $this->logger->critical($e); + $connection->rollBack(); + throw new \Magento\Sales\Exception\CouldNotInvoiceException( + __('Could not save an invoice, see error log for details') + ); + } + if ($notify) { + if (!$appendComment) { + $comment = null; + } + $this->notifierInterface->notify($order, $invoice, $comment); + } + return $invoice->getEntityId(); + } +} diff --git a/app/code/Magento/Sales/Model/Service/InvoiceService.php b/app/code/Magento/Sales/Model/Service/InvoiceService.php index 5d1f70310cdf7..ac66d4dc32f4c 100644 --- a/app/code/Magento/Sales/Model/Service/InvoiceService.php +++ b/app/code/Magento/Sales/Model/Service/InvoiceService.php @@ -143,8 +143,10 @@ public function prepareInvoice(Order $order, array $qtys = []) $qty = $orderItem->getQtyOrdered() ? $orderItem->getQtyOrdered() : 1; } elseif (isset($qtys[$orderItem->getId()])) { $qty = (double) $qtys[$orderItem->getId()]; - } else { + } elseif (empty($qtys)) { $qty = $orderItem->getQtyToInvoice(); + } else { + $qty = 0; } $totalQty += $qty; $this->setInvoiceItemQuantity($item, $qty); diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Invoice/PayOperationTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Invoice/PayOperationTest.php new file mode 100644 index 0000000000000..b97f2955f2f15 --- /dev/null +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Invoice/PayOperationTest.php @@ -0,0 +1,444 @@ +orderMock = $this->getMockForAbstractClass( + \Magento\Sales\Api\Data\OrderInterface::class, + [], + '', + false, + false, + true, + [ + 'getPayment', + 'setTotalInvoiced', + 'getTotalInvoiced', + 'setBaseTotalInvoiced', + 'getBaseTotalInvoiced', + 'setSubtotalInvoiced', + 'getSubtotalInvoiced', + 'setBaseSubtotalInvoiced', + 'getBaseSubtotalInvoiced', + 'setTaxInvoiced', + 'getTaxInvoiced', + 'setBaseTaxInvoiced', + 'getBaseTaxInvoiced', + 'setDiscountTaxCompensationInvoiced', + 'getDiscountTaxCompensationInvoiced', + 'setBaseDiscountTaxCompensationInvoiced', + 'getBaseDiscountTaxCompensationInvoiced', + 'setShippingTaxInvoiced', + 'getShippingTaxInvoiced', + 'setBaseShippingTaxInvoiced', + 'getBaseShippingTaxInvoiced', + 'setShippingInvoiced', + 'getShippingInvoiced', + 'setBaseShippingInvoiced', + 'getBaseShippingInvoiced', + 'setDiscountInvoiced', + 'getDiscountInvoiced', + 'setBaseDiscountInvoiced', + 'getBaseDiscountInvoiced', + 'setBaseTotalInvoicedCost', + 'getBaseTotalInvoicedCost' + ] + ); + $this->orderMock->expects($this->any()) + ->method('getTotalInvoiced') + ->willReturn(43); + $this->orderMock->expects($this->any()) + ->method('getBaseTotalInvoiced') + ->willReturn(43); + $this->orderMock->expects($this->any()) + ->method('getSubtotalInvoiced') + ->willReturn(22); + $this->orderMock->expects($this->any()) + ->method('getBaseSubtotalInvoiced') + ->willReturn(22); + $this->orderMock->expects($this->any()) + ->method('getTaxInvoiced') + ->willReturn(15); + $this->orderMock->expects($this->any()) + ->method('getBaseTaxInvoiced') + ->willReturn(15); + $this->orderMock->expects($this->any()) + ->method('getDiscountTaxCompensationInvoiced') + ->willReturn(11); + $this->orderMock->expects($this->any()) + ->method('getBaseDiscountTaxCompensationInvoiced') + ->willReturn(11); + $this->orderMock->expects($this->any()) + ->method('getShippingTaxInvoiced') + ->willReturn(12); + $this->orderMock->expects($this->any()) + ->method('getBaseShippingTaxInvoiced') + ->willReturn(12); + $this->orderMock->expects($this->any()) + ->method('getShippingInvoiced') + ->willReturn(28); + $this->orderMock->expects($this->any()) + ->method('getBaseShippingInvoiced') + ->willReturn(28); + $this->orderMock->expects($this->any()) + ->method('getDiscountInvoiced') + ->willReturn(19); + $this->orderMock->expects($this->any()) + ->method('getBaseDiscountInvoiced') + ->willReturn(19); + $this->orderMock->expects($this->any()) + ->method('getBaseTotalInvoicedCost') + ->willReturn(31); + + $this->invoiceMock = $this->getMockForAbstractClass( + \Magento\Sales\Api\Data\InvoiceInterface::class, + [], + '', + false, + false, + true, + [ + 'getItems', + 'getState', + 'capture', + 'setCanVoidFlag', + 'pay', + 'getGrandTotal', + 'getBaseGrandTotal', + 'getSubtotal', + 'getBaseSubtotal', + 'getTaxAmount', + 'getBaseTaxAmount', + 'getDiscountTaxCompensationAmount', + 'getBaseDiscountTaxCompensationAmount', + 'getShippingTaxAmount', + 'getBaseShippingTaxAmount', + 'getShippingAmount', + 'getBaseShippingAmount', + 'getDiscountAmount', + 'getBaseDiscountAmount', + 'getBaseCost' + ] + ); + $this->invoiceMock->expects($this->any()) + ->method('getGrandTotal') + ->willReturn(43); + $this->invoiceMock->expects($this->any()) + ->method('getBaseGrandTotal') + ->willReturn(43); + $this->invoiceMock->expects($this->any()) + ->method('getSubtotal') + ->willReturn(22); + $this->invoiceMock->expects($this->any()) + ->method('getBaseSubtotal') + ->willReturn(22); + $this->invoiceMock->expects($this->any()) + ->method('getTaxAmount') + ->willReturn(15); + $this->invoiceMock->expects($this->any()) + ->method('getBaseTaxAmount') + ->willReturn(15); + $this->invoiceMock->expects($this->any()) + ->method('getDiscountTaxCompensationAmount') + ->willReturn(11); + $this->invoiceMock->expects($this->any()) + ->method('getBaseDiscountTaxCompensationAmount') + ->willReturn(11); + $this->invoiceMock->expects($this->any()) + ->method('getShippingTaxAmount') + ->willReturn(12); + $this->invoiceMock->expects($this->any()) + ->method('getBaseShippingTaxAmount') + ->willReturn(12); + $this->invoiceMock->expects($this->any()) + ->method('getShippingAmount') + ->willReturn(28); + $this->invoiceMock->expects($this->any()) + ->method('getBaseShippingAmount') + ->willReturn(28); + $this->invoiceMock->expects($this->any()) + ->method('getDiscountAmount') + ->willReturn(19); + $this->invoiceMock->expects($this->any()) + ->method('getBaseDiscountAmount') + ->willReturn(19); + $this->invoiceMock->expects($this->any()) + ->method('getBaseCost') + ->willReturn(31); + + $this->contextMock = $this->getMock( + \Magento\Framework\Model\Context::class, + [], + [], + '', + false + ); + + $this->invoiceItemMock = $this->getMockForAbstractClass( + \Magento\Sales\Api\Data\InvoiceItemInterface::class, + [], + '', + false, + false, + true, + [ + 'isDeleted', + 'register' + ] + ); + $this->invoiceItemMock->expects($this->any()) + ->method('isDeleted') + ->willReturn(false); + $this->invoiceItemMock->expects($this->any()) + ->method('getQty') + ->willReturn(1); + + $this->orderPaymentMock = $this->getMockForAbstractClass( + \Magento\Sales\Api\Data\OrderPaymentInterface::class, + [], + '', + false, + false, + true, + [ + 'canCapture', + 'getMethodInstance', + 'getIsTransactionPending' + ] + ); + $this->orderMock->expects($this->any()) + ->method('getPayment') + ->willReturn($this->orderPaymentMock); + + $this->eventManagerMock = $this->getMockForAbstractClass( + \Magento\Framework\Event\ManagerInterface::class, + [], + '', + false, + false, + true, + [] + ); + $this->contextMock->expects($this->any()) + ->method('getEventDispatcher') + ->willReturn($this->eventManagerMock); + + $this->paymentMethodMock = $this->getMockForAbstractClass( + \Magento\Payment\Model\MethodInterface::class, + [], + '', + false, + false, + true, + [] + ); + $this->orderPaymentMock->expects($this->any()) + ->method('getMethodInstance') + ->willReturn($this->paymentMethodMock); + + $this->subject = new \Magento\Sales\Model\Order\Invoice\PayOperation( + $this->contextMock + ); + } + + /** + * @param bool|null $canCapture + * @param bool|null $isOnline + * @param bool|null $isGateway + * @param bool|null $isTransactionPending + * + * @dataProvider payDataProvider + * + * @return void + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testExecute($canCapture, $isOnline, $isGateway, $isTransactionPending) + { + $this->invoiceMock->expects($this->any()) + ->method('getItems') + ->willReturn([$this->invoiceItemMock]); + + if ($canCapture) { + $this->invoiceMock->expects($this->any()) + ->method('getState') + ->willReturn(\Magento\Sales\Model\Order\Invoice::STATE_OPEN); + + $this->orderPaymentMock->expects($this->any()) + ->method('canCapture') + ->willReturn(true); + + if ($isOnline) { + $this->invoiceMock->expects($this->once()) + ->method('capture'); + } else { + $this->invoiceMock->expects($this->never()) + ->method('capture'); + + $this->invoiceMock->expects($this->once()) + ->method('setCanVoidFlag') + ->with(false); + + $this->invoiceMock->expects($this->once()) + ->method('pay'); + } + } else { + $this->paymentMethodMock->expects($this->any()) + ->method('isGateway') + ->willReturn($isGateway); + + $this->orderPaymentMock->expects($this->any()) + ->method('getIsTransactionPending') + ->willReturn($isTransactionPending); + + $this->invoiceMock->expects($this->never()) + ->method('capture'); + + if ((!$isGateway || !$isOnline) && !$isTransactionPending) { + $this->invoiceMock->expects($this->once()) + ->method('setCanVoidFlag') + ->with(false); + + $this->invoiceMock->expects($this->once()) + ->method('pay'); + } + } + + $this->orderMock->expects($this->once()) + ->method('setTotalInvoiced') + ->with(86); + $this->orderMock->expects($this->once()) + ->method('setBaseTotalInvoiced') + ->with(86); + $this->orderMock->expects($this->once()) + ->method('setSubtotalInvoiced') + ->with(44); + $this->orderMock->expects($this->once()) + ->method('setBaseSubtotalInvoiced') + ->with(44); + $this->orderMock->expects($this->once()) + ->method('setTaxInvoiced') + ->with(30); + $this->orderMock->expects($this->once()) + ->method('setBaseTaxInvoiced') + ->with(30); + $this->orderMock->expects($this->once()) + ->method('setDiscountTaxCompensationInvoiced') + ->with(22); + $this->orderMock->expects($this->once()) + ->method('setBaseDiscountTaxCompensationInvoiced') + ->with(22); + $this->orderMock->expects($this->once()) + ->method('setShippingTaxInvoiced') + ->with(24); + $this->orderMock->expects($this->once()) + ->method('setBaseShippingTaxInvoiced') + ->with(24); + $this->orderMock->expects($this->once()) + ->method('setShippingInvoiced') + ->with(56); + $this->orderMock->expects($this->once()) + ->method('setBaseShippingInvoiced') + ->with(56); + $this->orderMock->expects($this->once()) + ->method('setDiscountInvoiced') + ->with(38); + $this->orderMock->expects($this->once()) + ->method('setBaseDiscountInvoiced') + ->with(38); + $this->orderMock->expects($this->once()) + ->method('setBaseTotalInvoicedCost') + ->with(62); + + $this->eventManagerMock->expects($this->once()) + ->method('dispatch') + ->with( + 'sales_order_invoice_register', + [ + 'invoice' => $this->invoiceMock, + 'order' => $this->orderMock + ] + ); + + $this->assertEquals( + $this->orderMock, + $this->subject->execute( + $this->orderMock, + $this->invoiceMock, + $isOnline + ) + ); + } + + /** + * @return array + */ + public function payDataProvider() + { + return [ + 'Invoice can capture, online' => [ + true, true, null, null + ], + 'Invoice can capture, offline' => [ + true, false, null, null + ], + 'Invoice can not capture, online, is not gateway, transaction is not pending' => [ + false, true, false, false + ], + 'Invoice can not capture, offline, gateway, transaction is not pending' => [ + false, false, true, false + ] + ]; + } +} diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Invoice/Sender/EmailSenderTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Invoice/Sender/EmailSenderTest.php new file mode 100644 index 0000000000000..0c72e8265b467 --- /dev/null +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Invoice/Sender/EmailSenderTest.php @@ -0,0 +1,359 @@ +orderMock = $this->getMockBuilder(\Magento\Sales\Model\Order::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->storeMock = $this->getMockBuilder(\Magento\Store\Model\Store::class) + ->setMethods(['getStoreId']) + ->disableOriginalConstructor() + ->getMock(); + + $this->storeMock->expects($this->any()) + ->method('getStoreId') + ->willReturn(1); + $this->orderMock->expects($this->any()) + ->method('getStore') + ->willReturn($this->storeMock); + + $this->senderMock = $this->getMockBuilder(\Magento\Sales\Model\Order\Email\Sender::class) + ->disableOriginalConstructor() + ->setMethods(['send', 'sendCopyTo']) + ->getMock(); + + $this->loggerMock = $this->getMockBuilder(\Psr\Log\LoggerInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->invoiceMock = $this->getMockBuilder(\Magento\Sales\Model\Order\Invoice::class) + ->disableOriginalConstructor() + ->setMethods(['setSendEmail', 'setEmailSent']) + ->getMock(); + + $this->commentMock = $this->getMockBuilder(\Magento\Sales\Api\Data\InvoiceCommentCreationInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->commentMock->expects($this->any()) + ->method('getComment') + ->willReturn('Comment text'); + + $this->addressMock = $this->getMockBuilder(\Magento\Sales\Model\Order\Address::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->orderMock->expects($this->any()) + ->method('getBillingAddress') + ->willReturn($this->addressMock); + $this->orderMock->expects($this->any()) + ->method('getShippingAddress') + ->willReturn($this->addressMock); + + $this->globalConfigMock = $this->getMockBuilder(\Magento\Framework\App\Config\ScopeConfigInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->eventManagerMock = $this->getMockBuilder(\Magento\Framework\Event\ManagerInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->paymentInfoMock = $this->getMockBuilder(\Magento\Payment\Model\Info::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->orderMock->expects($this->any()) + ->method('getPayment') + ->willReturn($this->paymentInfoMock); + + $this->paymentHelperMock = $this->getMockBuilder(\Magento\Payment\Helper\Data::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->paymentHelperMock->expects($this->any()) + ->method('getInfoBlockHtml') + ->with($this->paymentInfoMock, 1) + ->willReturn('Payment Info Block'); + + $this->invoiceResourceMock = $this->getMockBuilder(\Magento\Sales\Model\ResourceModel\Order\Invoice::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->addressRendererMock = $this->getMockBuilder(\Magento\Sales\Model\Order\Address\Renderer::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->addressRendererMock->expects($this->any()) + ->method('format') + ->with($this->addressMock, 'html') + ->willReturn('Formatted address'); + + $this->templateContainerMock = $this->getMockBuilder(\Magento\Sales\Model\Order\Email\Container\Template::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->identityContainerMock = $this->getMockBuilder( + \Magento\Sales\Model\Order\Email\Container\InvoiceIdentity::class + ) + ->disableOriginalConstructor() + ->getMock(); + + $this->identityContainerMock->expects($this->any()) + ->method('getStore') + ->willReturn($this->storeMock); + + $this->senderBuilderFactoryMock = $this->getMockBuilder( + \Magento\Sales\Model\Order\Email\SenderBuilderFactory::class + ) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + + $this->subject = new \Magento\Sales\Model\Order\Invoice\Sender\EmailSender( + $this->templateContainerMock, + $this->identityContainerMock, + $this->senderBuilderFactoryMock, + $this->loggerMock, + $this->addressRendererMock, + $this->paymentHelperMock, + $this->invoiceResourceMock, + $this->globalConfigMock, + $this->eventManagerMock + ); + } + + /** + * @param int $configValue + * @param bool $forceSyncMode + * @param bool $isComment + * @param bool $emailSendingResult + * + * @dataProvider sendDataProvider + * + * @return void + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testSend($configValue, $forceSyncMode, $isComment, $emailSendingResult) + { + $this->globalConfigMock->expects($this->once()) + ->method('getValue') + ->with('sales_email/general/async_sending') + ->willReturn($configValue); + + if (!$isComment) { + $this->commentMock = null; + } + + $this->invoiceMock->expects($this->once()) + ->method('setSendEmail') + ->with(true); + + if (!$configValue || $forceSyncMode) { + $transport = [ + 'order' => $this->orderMock, + 'invoice' => $this->invoiceMock, + 'comment' => $isComment ? 'Comment text' : '', + 'billing' => $this->addressMock, + 'payment_html' => 'Payment Info Block', + 'store' => $this->storeMock, + 'formattedShippingAddress' => 'Formatted address', + 'formattedBillingAddress' => 'Formatted address' + ]; + + $this->eventManagerMock->expects($this->once()) + ->method('dispatch') + ->with( + 'email_invoice_set_template_vars_before', + [ + 'sender' => $this->subject, + 'transport' => $transport + ] + ); + + $this->templateContainerMock->expects($this->once()) + ->method('setTemplateVars') + ->with($transport); + + $this->identityContainerMock->expects($this->once()) + ->method('isEnabled') + ->willReturn($emailSendingResult); + + if ($emailSendingResult) { + $this->senderBuilderFactoryMock->expects($this->once()) + ->method('create') + ->willReturn($this->senderMock); + + $this->senderMock->expects($this->once()) + ->method('send'); + + $this->senderMock->expects($this->once()) + ->method('sendCopyTo'); + + $this->invoiceMock->expects($this->once()) + ->method('setEmailSent') + ->with(true); + + $this->invoiceResourceMock->expects($this->once()) + ->method('saveAttribute') + ->with($this->invoiceMock, ['send_email', 'email_sent']); + + $this->assertTrue( + $this->subject->send( + $this->orderMock, + $this->invoiceMock, + $this->commentMock, + $forceSyncMode + ) + ); + } else { + $this->invoiceResourceMock->expects($this->once()) + ->method('saveAttribute') + ->with($this->invoiceMock, 'send_email'); + + $this->assertFalse( + $this->subject->send( + $this->orderMock, + $this->invoiceMock, + $this->commentMock, + $forceSyncMode + ) + ); + } + } else { + $this->invoiceMock->expects($this->once()) + ->method('setEmailSent') + ->with(null); + + $this->invoiceResourceMock->expects($this->at(0)) + ->method('saveAttribute') + ->with($this->invoiceMock, 'email_sent'); + $this->invoiceResourceMock->expects($this->at(1)) + ->method('saveAttribute') + ->with($this->invoiceMock, 'send_email'); + + $this->assertFalse( + $this->subject->send( + $this->orderMock, + $this->invoiceMock, + $this->commentMock, + $forceSyncMode + ) + ); + } + } + + /** + * @return array + */ + public function sendDataProvider() + { + return [ + 'Successful sync sending with comment' => [0, false, true, true], + 'Successful sync sending without comment' => [0, false, false, true], + 'Failed sync sending with comment' => [0, false, true, false], + 'Successful forced sync sending with comment' => [1, true, true, true], + 'Async sending' => [1, false, false, false] + ]; + } +} diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/InvoiceDocumentFactoryTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/InvoiceDocumentFactoryTest.php new file mode 100644 index 0000000000000..4247dc9567304 --- /dev/null +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/InvoiceDocumentFactoryTest.php @@ -0,0 +1,114 @@ +invoiceServiceMock = $this->getMockBuilder(InvoiceService::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->orderMock = $this->getMockBuilder(Order::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->invoiceMock = $this->getMockBuilder(InvoiceInterface::class) + ->disableOriginalConstructor() + ->setMethods(['addComment']) + ->getMockForAbstractClass(); + + $this->itemMock = $this->getMockBuilder(InvoiceItemCreationInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->commentMock = $this->getMockBuilder(InvoiceCommentCreationInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->invoiceDocumentFactory = new InvoiceDocumentFactory($this->invoiceServiceMock); + } + + public function testCreate() + { + $orderId = 10; + $orderQty = 3; + $comment = "Comment!"; + + $this->itemMock->expects($this->once()) + ->method('getOrderItemId') + ->willReturn($orderId); + + $this->itemMock->expects($this->once()) + ->method('getQty') + ->willReturn($orderQty); + + $this->invoiceMock->expects($this->once()) + ->method('addComment') + ->with($comment, null, null) + ->willReturnSelf(); + + $this->invoiceServiceMock->expects($this->once()) + ->method('prepareInvoice') + ->with($this->orderMock, [$orderId => $orderQty]) + ->willReturn($this->invoiceMock); + + $this->commentMock->expects($this->once()) + ->method('getComment') + ->willReturn($comment); + + $this->commentMock->expects($this->once()) + ->method('getIsVisibleOnFront') + ->willReturn(false); + + $this->assertEquals( + $this->invoiceMock, + $this->invoiceDocumentFactory->create($this->orderMock, [$this->itemMock], $this->commentMock) + ); + } +} diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/InvoiceValidatorTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/InvoiceValidatorTest.php new file mode 100644 index 0000000000000..4acbf55b1964c --- /dev/null +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/InvoiceValidatorTest.php @@ -0,0 +1,205 @@ +objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + + $this->orderValidatorMock = $this->getMockBuilder(\Magento\Sales\Model\Order\OrderValidatorInterface::class) + ->disableOriginalConstructor() + ->setMethods(['canInvoice']) + ->getMockForAbstractClass(); + + $this->orderMock = $this->getMockBuilder(\Magento\Sales\Api\Data\OrderInterface::class) + ->disableOriginalConstructor() + ->setMethods(['getStatus']) + ->getMockForAbstractClass(); + + $this->invoiceMock = $this->getMockBuilder(\Magento\Sales\Api\Data\InvoiceInterface::class) + ->disableOriginalConstructor() + ->setMethods(['getTotalQty', 'getItems']) + ->getMockForAbstractClass(); + + $this->model = $this->objectManager->getObject( + \Magento\Sales\Model\Order\InvoiceValidator::class, + ['orderValidator' => $this->orderValidatorMock] + ); + } + + public function testValidate() + { + $expectedResult = []; + $invoiceItemMock = $this->getInvoiceItemMock(1, 1); + $this->invoiceMock->expects($this->once()) + ->method('getItems') + ->willReturn([$invoiceItemMock]); + + $orderItemMock = $this->getOrderItemMock(1, 1, true); + $this->orderMock->expects($this->once()) + ->method('getItems') + ->willReturn([$orderItemMock]); + $this->orderValidatorMock->expects($this->once()) + ->method('canInvoice') + ->with($this->orderMock) + ->willReturn(true); + $this->assertEquals( + $expectedResult, + $this->model->validate($this->invoiceMock, $this->orderMock) + ); + } + + public function testValidateCanNotInvoiceOrder() + { + $orderStatus = 'Test Status'; + $expectedResult = [__('An invoice cannot be created when an order has a status of %1.', $orderStatus)]; + $invoiceItemMock = $this->getInvoiceItemMock(1, 1); + $this->invoiceMock->expects($this->once()) + ->method('getItems') + ->willReturn([$invoiceItemMock]); + + $orderItemMock = $this->getOrderItemMock(1, 1, true); + $this->orderMock->expects($this->once()) + ->method('getItems') + ->willReturn([$orderItemMock]); + $this->orderMock->expects($this->once()) + ->method('getStatus') + ->willReturn($orderStatus); + $this->orderValidatorMock->expects($this->once()) + ->method('canInvoice') + ->with($this->orderMock) + ->willReturn(false); + $this->assertEquals( + $expectedResult, + $this->model->validate($this->invoiceMock, $this->orderMock) + ); + } + + public function testValidateInvoiceQtyBiggerThanOrder() + { + $orderItemId = 1; + $message = 'The quantity to invoice must not be greater than the uninvoiced quantity for product SKU "%1".'; + $expectedResult = [__($message, $orderItemId)]; + $invoiceItemMock = $this->getInvoiceItemMock($orderItemId, 2); + $this->invoiceMock->expects($this->once()) + ->method('getItems') + ->willReturn([$invoiceItemMock]); + + $orderItemMock = $this->getOrderItemMock($orderItemId, 1, false); + $this->orderMock->expects($this->once()) + ->method('getItems') + ->willReturn([$orderItemMock]); + $this->orderValidatorMock->expects($this->once()) + ->method('canInvoice') + ->with($this->orderMock) + ->willReturn(true); + $this->assertEquals( + $expectedResult, + $this->model->validate($this->invoiceMock, $this->orderMock) + ); + } + + public function testValidateNoOrderItems() + { + $expectedResult = [__('The invoice contains one or more items that are not part of the original order.')]; + $invoiceItemMock = $this->getInvoiceItemMock(1, 1); + $this->invoiceMock->expects($this->once()) + ->method('getItems') + ->willReturn([$invoiceItemMock]); + + $this->orderMock->expects($this->once()) + ->method('getItems') + ->willReturn([]); + $this->orderValidatorMock->expects($this->once()) + ->method('canInvoice') + ->with($this->orderMock) + ->willReturn(true); + $this->assertEquals( + $expectedResult, + $this->model->validate($this->invoiceMock, $this->orderMock) + ); + } + + public function testValidateNoInvoiceItems() + { + $expectedResult = [__('You can\'t create an invoice without products.')]; + $orderItemId = 1; + $invoiceItemMock = $this->getInvoiceItemMock($orderItemId, 0); + $this->invoiceMock->expects($this->once()) + ->method('getItems') + ->willReturn([$invoiceItemMock]); + + $orderItemMock = $this->getOrderItemMock($orderItemId, 1, false); + $this->orderMock->expects($this->once()) + ->method('getItems') + ->willReturn([$orderItemMock]); + $this->orderValidatorMock->expects($this->once()) + ->method('canInvoice') + ->with($this->orderMock) + ->willReturn(true); + $this->assertEquals( + $expectedResult, + $this->model->validate($this->invoiceMock, $this->orderMock) + ); + } + + private function getInvoiceItemMock($orderItemId, $qty) + { + $invoiceItemMock = $this->getMockBuilder(\Magento\Sales\Api\Data\InvoiceItemInterface::class) + ->disableOriginalConstructor() + ->setMethods(['getOrderItemId', 'getQty']) + ->getMockForAbstractClass(); + $invoiceItemMock->expects($this->once())->method('getOrderItemId')->willReturn($orderItemId); + $invoiceItemMock->expects($this->once())->method('getQty')->willReturn($qty); + return $invoiceItemMock; + } + + private function getOrderItemMock($id, $qtyToInvoice, $isDummy) + { + $orderItemMock = $this->getMockBuilder(\Magento\Sales\Api\Data\OrderItemInterface::class) + ->disableOriginalConstructor() + ->setMethods(['getId', 'getQtyToInvoice', 'isDummy', 'getSku']) + ->getMockForAbstractClass(); + $orderItemMock->expects($this->any())->method('getId')->willReturn($id); + $orderItemMock->expects($this->any())->method('getQtyToInvoice')->willReturn($qtyToInvoice); + $orderItemMock->expects($this->any())->method('isDummy')->willReturn($isDummy); + $orderItemMock->expects($this->any())->method('getSku')->willReturn($id); + return $orderItemMock; + } +} diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/OrderValidatorTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/OrderValidatorTest.php new file mode 100644 index 0000000000000..5fb0d0a644eb7 --- /dev/null +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/OrderValidatorTest.php @@ -0,0 +1,146 @@ +objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + + $this->orderMock = $this->getMockBuilder(\Magento\Sales\Api\Data\OrderInterface::class) + ->disableOriginalConstructor() + ->setMethods(['getStatus', 'getItems']) + ->getMockForAbstractClass(); + + $this->orderItemMock = $this->getMockBuilder(\Magento\Sales\Api\Data\OrderItemInterface::class) + ->disableOriginalConstructor() + ->setMethods(['getQtyToInvoice', 'getLockedDoInvoice']) + ->getMockForAbstractClass(); + + $this->model = new \Magento\Sales\Model\Order\OrderValidator(); + } + + /** + * @param string $state + * + * @dataProvider canInvoiceWrongStateDataProvider + */ + public function testCanInvoiceWrongState($state) + { + $this->orderMock->expects($this->any()) + ->method('getState') + ->willReturn($state); + $this->orderMock->expects($this->never()) + ->method('getItems'); + $this->assertEquals( + false, + $this->model->canInvoice($this->orderMock) + ); + } + + /** + * Data provider for testCanInvoiceWrongState + * @return array + */ + public function canInvoiceWrongStateDataProvider() + { + return [ + [Order::STATE_PAYMENT_REVIEW], + [Order::STATE_HOLDED], + [Order::STATE_CANCELED], + [Order::STATE_COMPLETE], + [Order::STATE_CLOSED], + ]; + } + + public function testCanInvoiceNoItems() + { + $this->orderMock->expects($this->any()) + ->method('getState') + ->willReturn(Order::STATE_PROCESSING); + + $this->orderMock->expects($this->once()) + ->method('getItems') + ->willReturn([]); + + $this->assertEquals( + false, + $this->model->canInvoice($this->orderMock) + ); + } + + /** + * @param float $qtyToInvoice + * @param bool|null $itemLockedDoInvoice + * @param bool $expectedResult + * + * @dataProvider canInvoiceDataProvider + */ + public function testCanInvoice($qtyToInvoice, $itemLockedDoInvoice, $expectedResult) + { + $this->orderMock->expects($this->any()) + ->method('getState') + ->willReturn(Order::STATE_PROCESSING); + + $items = [$this->orderItemMock]; + $this->orderMock->expects($this->once()) + ->method('getItems') + ->willReturn($items); + $this->orderItemMock->expects($this->any()) + ->method('getQtyToInvoice') + ->willReturn($qtyToInvoice); + $this->orderItemMock->expects($this->any()) + ->method('getLockedDoInvoice') + ->willReturn($itemLockedDoInvoice); + + $this->assertEquals( + $expectedResult, + $this->model->canInvoice($this->orderMock) + ); + } + + /** + * Data provider for testCanInvoice + * + * @return array + */ + public function canInvoiceDataProvider() + { + return [ + [0, null, false], + [-1, null, false], + [1, true, false], + [0.5, false, true], + ]; + } +} diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/PaymentAdapterTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/PaymentAdapterTest.php new file mode 100644 index 0000000000000..2da2f4bba3f1a --- /dev/null +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/PaymentAdapterTest.php @@ -0,0 +1,70 @@ +orderMock = $this->getMockBuilder(\Magento\Sales\Api\Data\OrderInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->invoiceMock = $this->getMockBuilder(\Magento\Sales\Api\Data\InvoiceInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->payOperationMock =$this->getMockBuilder(\Magento\Sales\Model\Order\Invoice\PayOperation::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->subject = new \Magento\Sales\Model\Order\PaymentAdapter( + $this->payOperationMock + ); + } + + public function testPay() + { + $isOnline = true; + + $this->payOperationMock->expects($this->once()) + ->method('execute') + ->with($this->orderMock, $this->invoiceMock, $isOnline) + ->willReturn($this->orderMock); + + $this->assertEquals( + $this->orderMock, + $this->subject->pay( + $this->orderMock, + $this->invoiceMock, + $isOnline + ) + ); + } +} diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/StateResolverTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/StateResolverTest.php new file mode 100644 index 0000000000000..5d1958238b027 --- /dev/null +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/StateResolverTest.php @@ -0,0 +1,82 @@ +orderMock = $this->getMockBuilder(Order::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->orderStateResolver = new StateResolver(); + } + + public function testStateComplete() + { + $this->assertEquals(Order::STATE_COMPLETE, $this->orderStateResolver->getStateForOrder($this->orderMock)); + } + + public function testStateClosed() + { + $this->orderMock->expects($this->once()) + ->method('getBaseGrandTotal') + ->willReturn(100); + + $this->orderMock->expects($this->once()) + ->method('canCreditmemo') + ->willReturn(false); + + $this->orderMock->expects($this->once()) + ->method('getTotalRefunded') + ->willReturn(10.99); + + $this->assertEquals(Order::STATE_CLOSED, $this->orderStateResolver->getStateForOrder($this->orderMock)); + } + + public function testStateNew() + { + $this->orderMock->expects($this->once()) + ->method('isCanceled') + ->willReturn(true); + $this->assertEquals(Order::STATE_NEW, $this->orderStateResolver->getStateForOrder($this->orderMock)); + } + + public function testStateProcessing() + { + $arguments = [StateResolver::IN_PROGRESS]; + $this->orderMock->expects($this->once()) + ->method('isCanceled') + ->willReturn(true); + + $this->orderMock->expects($this->any()) + ->method('getState') + ->willReturn(Order::STATE_NEW); + + $this->assertEquals( + Order::STATE_PROCESSING, + $this->orderStateResolver->getStateForOrder($this->orderMock, $arguments) + ); + } +} diff --git a/app/code/Magento/Sales/Test/Unit/Model/OrderInvoiceTest.php b/app/code/Magento/Sales/Test/Unit/Model/OrderInvoiceTest.php new file mode 100644 index 0000000000000..3a3ac33afa8e2 --- /dev/null +++ b/app/code/Magento/Sales/Test/Unit/Model/OrderInvoiceTest.php @@ -0,0 +1,393 @@ +resourceConnectionMock = $this->getMockBuilder(ResourceConnection::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->orderRepositoryMock = $this->getMockBuilder(OrderRepositoryInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->invoiceDocumentFactoryMock = $this->getMockBuilder(InvoiceDocumentFactory::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->invoiceValidatorMock = $this->getMockBuilder(InvoiceValidatorInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->paymentAdapterMock = $this->getMockBuilder(PaymentAdapterInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->orderStateResolverMock = $this->getMockBuilder(OrderStateResolverInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->configMock = $this->getMockBuilder(OrderConfig::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->invoiceRepositoryMock = $this->getMockBuilder(InvoiceRepository::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->notifierInterfaceMock = $this->getMockBuilder(NotifierInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->loggerMock = $this->getMockBuilder(LoggerInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->invoiceCommentCreationMock = $this->getMockBuilder(InvoiceCommentCreationInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->invoiceCreationArgumentsMock = $this->getMockBuilder(InvoiceCreationArgumentsInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->orderMock = $this->getMockBuilder(OrderInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->invoiceMock = $this->getMockBuilder(InvoiceInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->adapterInterface = $this->getMockBuilder(AdapterInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->orderInvoice = new OrderInvoice( + $this->resourceConnectionMock, + $this->orderRepositoryMock, + $this->invoiceDocumentFactoryMock, + $this->invoiceValidatorMock, + $this->paymentAdapterMock, + $this->orderStateResolverMock, + $this->configMock, + $this->invoiceRepositoryMock, + $this->notifierInterfaceMock, + $this->loggerMock + ); + } + + /** + * @dataProvider dataProvider + */ + public function testOrderInvoice($orderId, $capture, $items, $notify, $appendComment) + { + $this->resourceConnectionMock->expects($this->once()) + ->method('getConnection') + ->with('sales') + ->willReturn($this->adapterInterface); + + $this->orderRepositoryMock->expects($this->once()) + ->method('get') + ->willReturn($this->orderMock); + + $this->invoiceDocumentFactoryMock->expects($this->once()) + ->method('create') + ->with( + $this->orderMock, + $items, + $this->invoiceCommentCreationMock, + ($appendComment && $notify), + $this->invoiceCreationArgumentsMock + )->willReturn($this->invoiceMock); + + $this->invoiceValidatorMock->expects($this->once()) + ->method('validate') + ->with($this->invoiceMock, $this->orderMock) + ->willReturn([]); + + $this->paymentAdapterMock->expects($this->once()) + ->method('pay') + ->with($this->orderMock, $this->invoiceMock, $capture) + ->willReturn($this->orderMock); + + $this->orderStateResolverMock->expects($this->once()) + ->method('getStateForOrder') + ->with($this->orderMock, [OrderStateResolverInterface::IN_PROGRESS]) + ->willReturn(Order::STATE_PROCESSING); + + $this->orderMock->expects($this->once()) + ->method('setState') + ->with(Order::STATE_PROCESSING) + ->willReturnSelf(); + + $this->orderMock->expects($this->once()) + ->method('getState') + ->willReturn(Order::STATE_PROCESSING); + + $this->configMock->expects($this->once()) + ->method('getStateDefaultStatus') + ->with(Order::STATE_PROCESSING) + ->willReturn('Processing'); + + $this->orderMock->expects($this->once()) + ->method('setStatus') + ->with('Processing') + ->willReturnSelf(); + + $this->invoiceMock->expects($this->once()) + ->method('setState') + ->with(\Magento\Sales\Model\Order\Invoice::STATE_PAID) + ->willReturnSelf(); + + $this->invoiceRepositoryMock->expects($this->once()) + ->method('save') + ->with($this->invoiceMock) + ->willReturn($this->invoiceMock); + + $this->orderRepositoryMock->expects($this->once()) + ->method('save') + ->with($this->orderMock) + ->willReturn($this->orderMock); + + if ($notify) { + $this->notifierInterfaceMock->expects($this->once()) + ->method('notify') + ->with($this->orderMock, $this->invoiceMock, $this->invoiceCommentCreationMock); + } + + $this->invoiceMock->expects($this->once()) + ->method('getEntityId') + ->willReturn(2); + + $this->assertEquals( + 2, + $this->orderInvoice->execute( + $orderId, + $capture, + $items, + $notify, + $appendComment, + $this->invoiceCommentCreationMock, + $this->invoiceCreationArgumentsMock + ) + ); + } + + /** + * @expectedException \Magento\Sales\Api\Exception\DocumentValidationExceptionInterface + */ + public function testDocumentValidationException() + { + $orderId = 1; + $capture = true; + $items = [1 => 2]; + $notify = true; + $appendComment = true; + $errorMessages = ['error1', 'error2']; + + $this->orderRepositoryMock->expects($this->once()) + ->method('get') + ->willReturn($this->orderMock); + + $this->invoiceDocumentFactoryMock->expects($this->once()) + ->method('create') + ->with( + $this->orderMock, + $items, + $this->invoiceCommentCreationMock, + ($appendComment && $notify), + $this->invoiceCreationArgumentsMock + )->willReturn($this->invoiceMock); + + $this->invoiceValidatorMock->expects($this->once()) + ->method('validate') + ->with($this->invoiceMock, $this->orderMock) + ->willReturn($errorMessages); + + $this->orderInvoice->execute( + $orderId, + $capture, + $items, + $notify, + $appendComment, + $this->invoiceCommentCreationMock, + $this->invoiceCreationArgumentsMock + ); + } + + /** + * @expectedException \Magento\Sales\Api\Exception\CouldNotInvoiceExceptionInterface + */ + public function testCouldNotInvoiceException() + { + $orderId = 1; + $items = [1 => 2]; + $capture = true; + $notify = true; + $appendComment = true; + $this->resourceConnectionMock->expects($this->once()) + ->method('getConnection') + ->with('sales') + ->willReturn($this->adapterInterface); + + $this->orderRepositoryMock->expects($this->once()) + ->method('get') + ->willReturn($this->orderMock); + + $this->invoiceDocumentFactoryMock->expects($this->once()) + ->method('create') + ->with( + $this->orderMock, + $items, + $this->invoiceCommentCreationMock, + ($appendComment && $notify), + $this->invoiceCreationArgumentsMock + )->willReturn($this->invoiceMock); + + $this->invoiceValidatorMock->expects($this->once()) + ->method('validate') + ->with($this->invoiceMock, $this->orderMock) + ->willReturn([]); + $e = new \Exception; + + $this->paymentAdapterMock->expects($this->once()) + ->method('pay') + ->with($this->orderMock, $this->invoiceMock, $capture) + ->willThrowException($e); + + $this->loggerMock->expects($this->once()) + ->method('critical') + ->with($e); + + $this->adapterInterface->expects($this->once()) + ->method('rollBack'); + + $this->orderInvoice->execute( + $orderId, + $capture, + $items, + $notify, + $appendComment, + $this->invoiceCommentCreationMock, + $this->invoiceCreationArgumentsMock + ); + } + + public function dataProvider() + { + return [ + 'TestWithNotifyTrue' => [1, true, [1 => 2], true, true], + 'TestWithNotifyFalse' => [1, true, [1 => 2], false, true] + ]; + } +} diff --git a/app/code/Magento/Sales/etc/di.xml b/app/code/Magento/Sales/etc/di.xml index 78dd17256703a..bd5fcf871d11b 100644 --- a/app/code/Magento/Sales/etc/di.xml +++ b/app/code/Magento/Sales/etc/di.xml @@ -49,7 +49,9 @@ + + @@ -61,9 +63,12 @@ + + + + - @@ -81,6 +86,10 @@ + + + + @@ -894,4 +903,11 @@ sales + + + + Magento\Sales\Model\Order\Invoice\Sender\EmailSender + + + diff --git a/app/code/Magento/Sales/etc/webapi.xml b/app/code/Magento/Sales/etc/webapi.xml index d6554a14d20cf..8d1b1fda5bc31 100644 --- a/app/code/Magento/Sales/etc/webapi.xml +++ b/app/code/Magento/Sales/etc/webapi.xml @@ -253,4 +253,10 @@ + + + + + + diff --git a/dev/tests/api-functional/phpunit.xml.dist b/dev/tests/api-functional/phpunit.xml.dist index 99754bb22cb8a..443cb704f1cae 100644 --- a/dev/tests/api-functional/phpunit.xml.dist +++ b/dev/tests/api-functional/phpunit.xml.dist @@ -35,7 +35,7 @@ - + diff --git a/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/OrderInvoiceCreateTest.php b/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/OrderInvoiceCreateTest.php new file mode 100644 index 0000000000000..85a034a5caa43 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/OrderInvoiceCreateTest.php @@ -0,0 +1,93 @@ +objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + + $this->invoiceRepository = $this->objectManager->get( + \Magento\Sales\Api\InvoiceRepositoryInterface::class + ); + } + + /** + * @magentoApiDataFixture Magento/Sales/_files/order_new.php + */ + public function testInvoiceCreate() + { + /** @var \Magento\Sales\Model\Order $existingOrder */ + $existingOrder = $this->objectManager->create(\Magento\Sales\Model\Order::class) + ->loadByIncrementId('100000001'); + + $serviceInfo = [ + 'rest' => [ + 'resourcePath' => '/V1/order/' . $existingOrder->getId() . '/invoice', + 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_POST + ], + 'soap' => [ + 'service' => self::SERVICE_READ_NAME, + 'serviceVersion' => self::SERVICE_VERSION, + 'operation' => self::SERVICE_READ_NAME . 'execute' + ] + ]; + + $requestData = [ + 'orderId' => $existingOrder->getId(), + 'items' => [], + 'comment' => [ + 'comment' => 'Test Comment', + 'is_visible_on_front' => 1 + ] + ]; + + /** @var \Magento\Sales\Api\Data\OrderItemInterface $item */ + foreach ($existingOrder->getAllItems() as $item) { + $requestData['items'][] = [ + 'order_item_id' => $item->getItemId(), + 'qty' => $item->getQtyOrdered() + ]; + } + + $result = $this->_webApiCall($serviceInfo, $requestData); + + $this->assertNotEmpty($result); + + try { + $this->invoiceRepository->get($result); + } catch (\Magento\Framework\Exception\NoSuchEntityException $e) { + $this->fail('Failed asserting that Invoice was created'); + } + + /** @var \Magento\Sales\Model\Order $updatedOrder */ + $updatedOrder = $this->objectManager->create(\Magento\Sales\Model\Order::class) + ->loadByIncrementId('100000001'); + + $this->assertNotEquals( + $existingOrder->getStatus(), + $updatedOrder->getStatus(), + 'Failed asserting that Order status was changed' + ); + } +} diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Adminhtml/Product/Attribute/CustomAttribute.php b/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Adminhtml/Product/Attribute/CustomAttribute.php index 91d722f8ef4eb..d624fafa3143a 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Adminhtml/Product/Attribute/CustomAttribute.php +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Adminhtml/Product/Attribute/CustomAttribute.php @@ -8,6 +8,9 @@ use Magento\Mtf\Client\Locator; use Magento\Mtf\Client\Element\SimpleElement; +use Magento\Mtf\Client\DriverInterface; +use Magento\Mtf\Client\ElementInterface; +use Magento\Mtf\System\Event\EventManagerInterface; /** * Catalog product custom attribute element. @@ -15,25 +18,45 @@ class CustomAttribute extends SimpleElement { /** - * Attribute input selector; + * Attribute input selector. * * @var string */ - protected $inputSelector = '[name="product[%s]"]'; + private $inputSelector = '[name="product[%s]"]'; + + /** + * Locator for data grid. + * + * @var string + */ + private $dataGrid = '[data-role="grid"]'; /** * Attribute class to element type reference. * * @var array */ - protected $classReference = [ - 'admin__control-text' => null, - 'textarea' => null, - 'hasDatepicker' => 'datepicker', - 'admin__control-select' => 'select', - 'admin__control-multiselect' => 'multiselect', - 'admin__actions-switch-checkbox' => 'switcher' - ]; + private $classReferences = []; + + /** + * Constructor + * + * @param DriverInterface $driver + * @param EventManagerInterface $eventManager + * @param Locator $locator + * @param ElementInterface $context + * @param array $classReferences + */ + public function __construct( + DriverInterface $driver, + EventManagerInterface $eventManager, + Locator $locator, + ElementInterface $context, + array $classReferences + ) { + parent::__construct($driver, $eventManager, $locator, $context); + $this->classReferences = $classReferences; + } /** * Set attribute value. @@ -48,7 +71,11 @@ public function setValue($data) $element = $this->getElementByClass($this->getElementClass($code)); $value = is_array($data) ? $data['value'] : $data; if ($value !== null) { - $this->find(sprintf($this->inputSelector, $code), Locator::SELECTOR_CSS, $element)->setValue($value); + $this->find( + str_replace('%code%', $code, $element['selector']), + Locator::SELECTOR_CSS, + $element['type'] + )->setValue($value); } } @@ -66,17 +93,17 @@ public function getValue() } /** - * Get element type by class. + * Get element by class. * * @param string $class - * @return string + * @return array|null */ private function getElementByClass($class) { $element = null; - foreach ($this->classReference as $key => $reference) { + foreach (array_keys($this->classReferences) as $key) { if (strpos($class, $key) !== false) { - $element = $reference; + return $this->classReferences[$class]; } } return $element; @@ -90,7 +117,9 @@ private function getElementByClass($class) */ private function getElementClass($code) { - return $this->find(sprintf($this->inputSelector, $code), Locator::SELECTOR_CSS)->getAttribute('class'); + return $this->find($this->dataGrid)->isVisible() + ? 'dynamicRows' + : $this->find(sprintf($this->inputSelector, $code), Locator::SELECTOR_CSS)->getAttribute('class'); } /** diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/Fixture/CatalogProductSimple/CustomAttribute.php b/dev/tests/functional/tests/app/Magento/Catalog/Test/Fixture/CatalogProductSimple/CustomAttribute.php index a28a65de925a7..f8be03d37846e 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/Fixture/CatalogProductSimple/CustomAttribute.php +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/Fixture/CatalogProductSimple/CustomAttribute.php @@ -20,7 +20,7 @@ class CustomAttribute extends DataSource * * @var CatalogProductAttribute */ - protected $attribute; + private $attribute; /** * @constructor @@ -54,7 +54,7 @@ public function __construct(FixtureFactory $fixtureFactory, array $params, $data * @param CatalogProductAttribute $attribute * @return string|null */ - protected function getDefaultAttributeValue(CatalogProductAttribute $attribute) + private function getDefaultAttributeValue(CatalogProductAttribute $attribute) { $data = $attribute->getData(); $value = ''; @@ -91,7 +91,7 @@ public function getAttribute() * @param CatalogProductAttribute $attribute * @return string */ - protected function createAttributeCode(CatalogProductAttribute $attribute) + private function createAttributeCode(CatalogProductAttribute $attribute) { $label = $attribute->getFrontendLabel(); return strtolower(preg_replace('@[\W\s]+@', '_', $label)); diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/ProductAttribute/CreateProductAttributeEntityTest.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/ProductAttribute/CreateProductAttributeEntityTest.xml index d61ef9004b2f1..2a94a12c0dbdf 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/ProductAttribute/CreateProductAttributeEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/ProductAttribute/CreateProductAttributeEntityTest.xml @@ -174,6 +174,12 @@ Fixed Product Tax attr_fpt_code_%isolation% Fixed_Product_Tax_Storeview + + United States + 10 + All Websites USD + * + diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/etc/di.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/etc/di.xml index 2356222387920..82a7c263b1ca9 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/etc/di.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/etc/di.xml @@ -156,4 +156,34 @@ low + + + + + [name="product[%code%]"] + null + + + [name="product[%code%]"] + null + + + [name="product[%code%]"] + datepicker + + + [name="product[%code%]"] + select + + + [name="product[%code%]"] + multiselect + + + [name="product[%code%]"] + switcher + + + + diff --git a/dev/tests/functional/tests/app/Magento/Weee/Test/Block/Adminhtml/Product/Edit/Section/ProductDetails/Fpt.php b/dev/tests/functional/tests/app/Magento/Weee/Test/Block/Adminhtml/Product/Edit/Section/ProductDetails/Fpt.php new file mode 100644 index 0000000000000..e059563ff0fc4 --- /dev/null +++ b/dev/tests/functional/tests/app/Magento/Weee/Test/Block/Adminhtml/Product/Edit/Section/ProductDetails/Fpt.php @@ -0,0 +1,70 @@ + [ + 'type' => 'select', + 'selector' => '[name$="[country]"]' + ], + 'website' => [ + 'type' => 'select', + 'selector' => '[name$="[website_id]"]' + ], + 'tax' => [ + 'type' => 'input', + 'selector' => '[name$="[value]"]' + ], + 'state' => [ + 'type' => 'select', + 'selector' => '[name$="[state]"]' + ] + ]; + + /** + * Fill Fixed Product Tax form. + * + * @param string|array $value + * @return void + */ + public function setValue($value) + { + if ($this->find($this->buttonFormLocator)->isVisible()) { + $this->find($this->buttonFormLocator)->click(); + } + foreach ((array)$value as $name => $data) { + $element = $this->find( + $this->fields[$name]['selector'], + Locator::SELECTOR_CSS, + $this->fields[$name]['type'] + ); + + if ($element->isVisible()) { + $element->setValue($data); + } + } + } +} diff --git a/dev/tests/functional/tests/app/Magento/Weee/Test/etc/di.xml b/dev/tests/functional/tests/app/Magento/Weee/Test/etc/di.xml index 2097566cc2463..30be698de32ea 100644 --- a/dev/tests/functional/tests/app/Magento/Weee/Test/etc/di.xml +++ b/dev/tests/functional/tests/app/Magento/Weee/Test/etc/di.xml @@ -6,9 +6,19 @@ */ --> - - - high - - + + + high + + + + + + + [data-role="grid"] + \Magento\Weee\Test\Block\Adminhtml\Product\Edit\Section\ProductDetails\Fpt + + + + diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/order_new.php b/dev/tests/integration/testsuite/Magento/Sales/_files/order_new.php new file mode 100644 index 0000000000000..246d48feaafe3 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/order_new.php @@ -0,0 +1,24 @@ +create('Magento\Sales\Model\Order') + ->loadByIncrementId('100000001'); + +$order->setState( + \Magento\Sales\Model\Order::STATE_NEW +); + +$order->setStatus( + $order->getConfig()->getStateDefaultStatus( + \Magento\Sales\Model\Order::STATE_NEW + ) +); + +$order->save(); diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/order_new_rollback.php b/dev/tests/integration/testsuite/Magento/Sales/_files/order_new_rollback.php new file mode 100644 index 0000000000000..7603ca732c71f --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/order_new_rollback.php @@ -0,0 +1,6 @@ +_useFtp ? new \Magento\Framework\Backup\Filesystem\Rollback\Ftp( - $this - ) : new \Magento\Framework\Backup\Filesystem\Rollback\Fs( - $this - ); + $rollbackWorker = $this->_useFtp ? $this->getRollBackFtp() : $this->getRollBackFs(); $rollbackWorker->run(); $this->_lastOperationSucceed = true; @@ -299,4 +307,36 @@ protected function _getTarTmpPath() $tmpName = '~tmp-' . microtime(true) . '.tar'; return $this->getBackupsDir() . '/' . $tmpName; } + + /** + * @return \Magento\Framework\Backup\Filesystem\Rollback\Ftp + * @deprecated + */ + protected function getRollBackFtp() + { + if (!$this->rollBackFtp) { + $this->rollBackFtp = ObjectManager::getInstance()->create( + \Magento\Framework\Backup\Filesystem\Rollback\Ftp::class, + [$this] + ); + } + + return $this->rollBackFtp; + } + + /** + * @return \Magento\Framework\Backup\Filesystem\Rollback\Fs + * @deprecated + */ + protected function getRollBackFs() + { + if (!$this->rollBackFs) { + $this->rollBackFs = ObjectManager::getInstance()->create( + \Magento\Framework\Backup\Filesystem\Rollback\Fs::class, + [$this] + ); + } + + return $this->rollBackFs; + } } diff --git a/lib/internal/Magento/Framework/Backup/Filesystem/Helper.php b/lib/internal/Magento/Framework/Backup/Filesystem/Helper.php index 355f10226622a..2587cd543818a 100644 --- a/lib/internal/Magento/Framework/Backup/Filesystem/Helper.php +++ b/lib/internal/Magento/Framework/Backup/Filesystem/Helper.php @@ -87,10 +87,12 @@ public function getInfo($path, $infoOptions = self::INFO_ALL, $skipFiles = []) $info = []; if ($infoOptions & self::INFO_READABLE) { $info['readable'] = true; + $info['readableMeta'] = []; } if ($infoOptions & self::INFO_WRITABLE) { $info['writable'] = true; + $info['writableMeta'] = []; } if ($infoOptions & self::INFO_SIZE) { @@ -111,10 +113,12 @@ public function getInfo($path, $infoOptions = self::INFO_ALL, $skipFiles = []) if ($infoOptions & self::INFO_WRITABLE && !$item->isWritable()) { $info['writable'] = false; + $info['writableMeta'][] = $item->getPathname(); } if ($infoOptions & self::INFO_READABLE && !$item->isReadable()) { $info['readable'] = false; + $info['readableMeta'][] = $item->getPathname(); } if ($infoOptions & self::INFO_SIZE && !$item->isDir()) { diff --git a/lib/internal/Magento/Framework/Backup/Filesystem/Rollback/Fs.php b/lib/internal/Magento/Framework/Backup/Filesystem/Rollback/Fs.php index 0339195bbb96a..fe627b3dd59dc 100644 --- a/lib/internal/Magento/Framework/Backup/Filesystem/Rollback/Fs.php +++ b/lib/internal/Magento/Framework/Backup/Filesystem/Rollback/Fs.php @@ -5,6 +5,8 @@ */ namespace Magento\Framework\Backup\Filesystem\Rollback; +use Magento\Framework\App\ObjectManager; + /** * Rollback worker for rolling back via local filesystem * @@ -12,6 +14,11 @@ */ class Fs extends AbstractRollback { + /** + * @var \Magento\Framework\Backup\Filesystem\Helper + */ + private $fsHelper; + /** * Files rollback implementation via local filesystem * @@ -30,7 +37,7 @@ public function run() ); } - $fsHelper = new \Magento\Framework\Backup\Filesystem\Helper(); + $fsHelper = $this->getFsHelper(); $filesInfo = $fsHelper->getInfo( $this->_snapshot->getRootDir(), @@ -39,6 +46,15 @@ public function run() ); if (!$filesInfo['writable']) { + if (!empty($filesInfo['writableMeta'])) { + throw new \Magento\Framework\Backup\Exception\NotEnoughPermissions( + new \Magento\Framework\Phrase( + 'You need write permissions for: %1', + [implode(', ', $filesInfo['writableMeta'])] + ) + ); + } + throw new \Magento\Framework\Backup\Exception\NotEnoughPermissions( new \Magento\Framework\Phrase('Unable to make rollback because not all files are writable') ); @@ -59,4 +75,17 @@ public function run() $fsHelper->rm($this->_snapshot->getRootDir(), $this->_snapshot->getIgnorePaths()); $archiver->unpack($snapshotPath, $this->_snapshot->getRootDir()); } + + /** + * @return \Magento\Framework\Backup\Filesystem\Helper + * @deprecated + */ + private function getFsHelper() + { + if (!$this->fsHelper) { + $this->fsHelper = ObjectManager::getInstance()->get(\Magento\Framework\Backup\Filesystem\Helper::class); + } + + return $this->fsHelper; + } } diff --git a/lib/internal/Magento/Framework/Backup/Test/Unit/Filesystem/Rollback/FsTest.php b/lib/internal/Magento/Framework/Backup/Test/Unit/Filesystem/Rollback/FsTest.php new file mode 100644 index 0000000000000..dbd830a8fc9f2 --- /dev/null +++ b/lib/internal/Magento/Framework/Backup/Test/Unit/Filesystem/Rollback/FsTest.php @@ -0,0 +1,111 @@ +backupPath = '/some/test/path'; + $this->rootDir = '/'; + $this->ignorePaths = []; + + $this->objectManager = new ObjectManager($this); + $this->snapshotMock = $this->getMockBuilder(\Magento\Framework\Backup\Filesystem::class) + ->setMethods(['getBackupPath', 'getRootDir', 'getIgnorePaths']) + ->getMock(); + $this->snapshotMock->expects($this->any()) + ->method('getBackupPath') + ->willReturn($this->backupPath); + $this->snapshotMock->expects($this->any()) + ->method('getRootDir') + ->willReturn($this->rootDir); + $this->snapshotMock->expects($this->any()) + ->method('getIgnorePaths') + ->willReturn($this->ignorePaths); + $this->fsHelperMock = $this->getMockBuilder(\Magento\Framework\Backup\Filesystem\Helper::class) + ->setMethods(['getInfo', 'rm']) + ->getMock(); + $this->fs = $this->objectManager->getObject( + \Magento\Framework\Backup\Filesystem\Rollback\Fs::class, + [ + 'snapshotObject' => $this->snapshotMock, + 'fsHelper' => $this->fsHelperMock + ] + ); + + } + + /** + * @expectedException \Magento\Framework\Backup\Exception\NotEnoughPermissions + * @expectedExceptionMessage You need write permissions for: test1, test2 + */ + public function testRunNotEnoughPermissions() + { + $fsInfo = [ + 'writable' => false, + 'writableMeta' => ['test1', 'test2'] + ]; + + $this->fsHelperMock->expects($this->once()) + ->method('getInfo') + ->willReturn($fsInfo); + $this->fs->run(); + } + + public function testRun() + { + $fsInfo = ['writable' => true]; + + $this->fsHelperMock->expects($this->once()) + ->method('getInfo') + ->willReturn($fsInfo); + $this->fsHelperMock->expects($this->once()) + ->method('rm') + ->with($this->rootDir, $this->ignorePaths); + + $this->fs->run(); + } +} diff --git a/lib/internal/Magento/Framework/Backup/Test/Unit/Filesystem/Rollback/_files/ioMock.php b/lib/internal/Magento/Framework/Backup/Test/Unit/Filesystem/Rollback/_files/ioMock.php new file mode 100644 index 0000000000000..3ac56872e87b9 --- /dev/null +++ b/lib/internal/Magento/Framework/Backup/Test/Unit/Filesystem/Rollback/_files/ioMock.php @@ -0,0 +1,28 @@ +objectManager = new ObjectManager($this); + $this->fsMock = $this->getMockBuilder(\Magento\Framework\Backup\Filesystem\Rollback\Fs::class) + ->disableOriginalConstructor() + ->getMock(); + $this->ftpMock = $this->getMockBuilder(\Magento\Framework\Backup\Filesystem\Rollback\Ftp::class) + ->disableOriginalConstructor() + ->getMock(); + $this->snapshotMock = $this->getMockBuilder(\Magento\Framework\Backup\Filesystem::class) + ->getMock(); + $this->filesystem = $this->objectManager->getObject( + \Magento\Framework\Backup\Filesystem::class, + [ + 'rollBackFtp' => $this->ftpMock, + 'rollBackFs' => $this->fsMock + ] + ); + } + + public function testRollback() + { + $this->assertTrue($this->filesystem->rollback()); + } +} diff --git a/lib/internal/Magento/Framework/Backup/Test/Unit/MediaTest.php b/lib/internal/Magento/Framework/Backup/Test/Unit/MediaTest.php index ef382be2d4b52..a53d3fded0a41 100644 --- a/lib/internal/Magento/Framework/Backup/Test/Unit/MediaTest.php +++ b/lib/internal/Magento/Framework/Backup/Test/Unit/MediaTest.php @@ -5,10 +5,17 @@ */ namespace Magento\Framework\Backup\Test\Unit; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; + require_once __DIR__ . '/_files/io.php'; class MediaTest extends \PHPUnit_Framework_TestCase { + /** + * @var \Magento\Framework\TestFramework\Unit\Helper\ObjectManager + */ + private $objectManager; + /** * @var \Magento\Framework\Filesystem|\PHPUnit_Framework_MockObject_MockObject */ @@ -24,6 +31,11 @@ class MediaTest extends \PHPUnit_Framework_TestCase */ protected $_backupDbMock; + /** + * @var \Magento\Framework\Backup\Filesystem\Rollback\Fs + */ + private $fsMock; + public static function setUpBeforeClass() { require __DIR__ . '/_files/app_dirs.php'; @@ -36,6 +48,7 @@ public static function tearDownAfterClass() protected function setUp() { + $this->objectManager = new ObjectManager($this); $this->_backupDbMock = $this->getMock(\Magento\Framework\Backup\Db::class, [], [], '', false); $this->_backupDbMock->expects($this->any())->method('setBackupExtension')->will($this->returnSelf()); @@ -69,6 +82,8 @@ protected function setUp() )->will( $this->returnValue($this->_backupDbMock) ); + + $this->fsMock = $this->getMock(\Magento\Framework\Backup\Filesystem\Rollback\Fs::class, [], [], '', false); } /** @@ -81,7 +96,14 @@ public function testAction($action) $rootDir = str_replace('\\', '/', TESTS_TEMP_DIR) . '/Magento/Backup/data'; - $model = new \Magento\Framework\Backup\Media($this->_filesystemMock, $this->_backupFactoryMock); + $model = $this->objectManager->getObject( + \Magento\Framework\Backup\Media::class, + [ + 'filesystem' => $this->_filesystemMock, + 'backupFactory' => $this->_backupFactoryMock, + 'rollBackFs' => $this->fsMock + ] + ); $model->setRootDir($rootDir); $model->setBackupsDir($rootDir); $model->{$action}(); diff --git a/lib/internal/Magento/Framework/Backup/Test/Unit/NomediaTest.php b/lib/internal/Magento/Framework/Backup/Test/Unit/NomediaTest.php index 6a045a9974f84..228ea9a5569b3 100644 --- a/lib/internal/Magento/Framework/Backup/Test/Unit/NomediaTest.php +++ b/lib/internal/Magento/Framework/Backup/Test/Unit/NomediaTest.php @@ -5,10 +5,17 @@ */ namespace Magento\Framework\Backup\Test\Unit; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; + require_once __DIR__ . '/_files/io.php'; class NomediaTest extends \PHPUnit_Framework_TestCase { + /** + * @var \Magento\Framework\TestFramework\Unit\Helper\ObjectManager + */ + private $objectManager; + /** * @var \Magento\Framework\Filesystem */ @@ -24,6 +31,11 @@ class NomediaTest extends \PHPUnit_Framework_TestCase */ protected $_backupDbMock; + /** + * @var \Magento\Framework\Backup\Filesystem\Rollback\Fs + */ + private $fsMock; + public static function setUpBeforeClass() { require __DIR__ . '/_files/app_dirs.php'; @@ -36,6 +48,7 @@ public static function tearDownAfterClass() protected function setUp() { + $this->objectManager = new ObjectManager($this); $this->_backupDbMock = $this->getMock(\Magento\Framework\Backup\Db::class, [], [], '', false); $this->_backupDbMock->expects($this->any())->method('setBackupExtension')->will($this->returnSelf()); @@ -69,6 +82,8 @@ protected function setUp() )->will( $this->returnValue($this->_backupDbMock) ); + + $this->fsMock = $this->getMock(\Magento\Framework\Backup\Filesystem\Rollback\Fs::class, [], [], '', false); } /** @@ -81,7 +96,14 @@ public function testAction($action) $rootDir = TESTS_TEMP_DIR . '/Magento/Backup/data'; - $model = new \Magento\Framework\Backup\Nomedia($this->_filesystemMock, $this->_backupFactoryMock); + $model = $this->objectManager->getObject( + \Magento\Framework\Backup\Nomedia::class, + [ + 'filesystem' => $this->_filesystemMock, + 'backupFactory' => $this->_backupFactoryMock, + 'rollBackFs' => $this->fsMock + ] + ); $model->setRootDir($rootDir); $model->setBackupsDir($rootDir); $model->{$action}(); diff --git a/lib/internal/Magento/Framework/Filesystem/Directory/Write.php b/lib/internal/Magento/Framework/Filesystem/Directory/Write.php index 0a46a17c10b22..25486562257ca 100644 --- a/lib/internal/Magento/Framework/Filesystem/Directory/Write.php +++ b/lib/internal/Magento/Framework/Filesystem/Directory/Write.php @@ -40,7 +40,7 @@ public function __construct( } /** - * Check if directory is writable + * Check if directory or file is writable * * @param string $path * @return void @@ -49,7 +49,9 @@ public function __construct( protected function assertWritable($path) { if ($this->isWritable($path) === false) { - $path = $this->getAbsolutePath($this->path, $path); + $path = (!$this->driver->isFile($path)) + ? $this->getAbsolutePath($this->path, $path) + : $this->getAbsolutePath($path); throw new FileSystemException(new \Magento\Framework\Phrase('The path "%1" is not writable', [$path])); } } @@ -240,12 +242,13 @@ public function isWritable($path = null) * @param string $path * @param string $mode * @return \Magento\Framework\Filesystem\File\WriteInterface + * @throws \Magento\Framework\Exception\FileSystemException */ public function openFile($path, $mode = 'w') { $folder = dirname($path); $this->create($folder); - $this->assertWritable($folder); + $this->assertWritable($this->isExist($path) ? $path : $folder); $absolutePath = $this->driver->getAbsolutePath($this->path, $path); return $this->fileFactory->create($absolutePath, $this->driver, $mode); } diff --git a/lib/internal/Magento/Framework/Filesystem/Test/Unit/Directory/WriteTest.php b/lib/internal/Magento/Framework/Filesystem/Test/Unit/Directory/WriteTest.php index 5c93c890f04de..1a84fb6a69a6e 100644 --- a/lib/internal/Magento/Framework/Filesystem/Test/Unit/Directory/WriteTest.php +++ b/lib/internal/Magento/Framework/Filesystem/Test/Unit/Directory/WriteTest.php @@ -121,6 +121,17 @@ public function testCreateSymlinkTargetDirectoryExists() $this->assertTrue($this->write->createSymlink($sourcePath, $destinationFile, $targetDir)); } + /** + * @expectedException \Magento\Framework\Exception\FileSystemException + */ + public function testOpenFileNonWritable() + { + $targetPath = '/path/to/target.file'; + $this->driver->expects($this->once())->method('isExists')->willReturn(true); + $this->driver->expects($this->once())->method('isWritable')->willReturn(false); + $this->write->openFile($targetPath); + } + /** * Assert is file expectation *