diff --git a/app/code/Magento/ImportExport/Controller/Adminhtml/Import/Validate.php b/app/code/Magento/ImportExport/Controller/Adminhtml/Import/Validate.php
index 4bd13d6264cb0..7cf77449dcdbe 100644
--- a/app/code/Magento/ImportExport/Controller/Adminhtml/Import/Validate.php
+++ b/app/code/Magento/ImportExport/Controller/Adminhtml/Import/Validate.php
@@ -46,7 +46,7 @@ public function execute()
$import->uploadSource(),
$this->_objectManager->create(\Magento\Framework\Filesystem::class)
->getDirectoryWrite(DirectoryList::ROOT),
- $data[$import::FIELD_FIELD_SEPARATOR]
+ $this->getSourceAdapterOptions($data)
);
$this->processValidationResult($import->validateSource($source), $resultBlock);
} catch (\Magento\Framework\Exception\LocalizedException $e) {
@@ -120,6 +120,19 @@ private function getImport()
return $this->import;
}
+ /**
+ * Returns options for source adapter
+ *
+ * @param $data
+ * @return array
+ */
+ protected function getSourceAdapterOptions($data)
+ {
+ return [
+ 'delimiter' => $data[Import::FIELD_FIELD_SEPARATOR] ?: ','
+ ];
+ }
+
/**
* Add error message to Result block and allow 'Import' button
*
diff --git a/app/code/Magento/ImportExport/Model/Import.php b/app/code/Magento/ImportExport/Model/Import.php
index 60544a781b34a..17653c67c299e 100644
--- a/app/code/Magento/ImportExport/Model/Import.php
+++ b/app/code/Magento/ImportExport/Model/Import.php
@@ -267,7 +267,7 @@ protected function _getSourceAdapter($sourceFile)
return \Magento\ImportExport\Model\Import\Adapter::findAdapterFor(
$sourceFile,
$this->_filesystem->getDirectoryWrite(DirectoryList::ROOT),
- $this->getData(self::FIELD_FIELD_SEPARATOR)
+ $this->getSourceAdapterOptions()
);
}
@@ -785,4 +785,16 @@ public function getDeletedItemsCount()
{
return $this->_getEntityAdapter()->getDeletedItemsCount();
}
+
+ /**
+ * Returns options for source adapter
+ *
+ * @return array
+ */
+ protected function getSourceAdapterOptions()
+ {
+ return [
+ 'delimiter' => $this->getData(self::FIELD_FIELD_SEPARATOR) ?: ','
+ ];
+ }
}
diff --git a/app/code/Magento/ImportExport/Model/Import/Adapter.php b/app/code/Magento/ImportExport/Model/Import/Adapter.php
index 0242ae6096316..5d3e483e08f81 100644
--- a/app/code/Magento/ImportExport/Model/Import/Adapter.php
+++ b/app/code/Magento/ImportExport/Model/Import/Adapter.php
@@ -5,7 +5,11 @@
*/
namespace Magento\ImportExport\Model\Import;
+use Magento\Framework\App\ObjectManager;
use Magento\Framework\Filesystem\Directory\Write;
+use Magento\ImportExport\Model\Import\Source\FileFactory;
+use Magento\ImportExport\Model\Import\Source\FileParser\CorruptedFileException;
+use Magento\ImportExport\Model\Import\Source\FileParser\UnsupportedPathException;
/**
* Import adapter model
@@ -14,6 +18,15 @@
*/
class Adapter
{
+ /** @var FileFactory */
+ private $fileSourceFactory;
+
+ public function __construct(FileFactory $fileSourceFactory)
+ {
+ $this->fileSourceFactory = $fileSourceFactory;
+ }
+
+
/**
* Adapter factory. Checks for availability, loads and create instance of import adapter object.
*
@@ -25,29 +38,12 @@ class Adapter
* @return AbstractSource
*
* @throws \Magento\Framework\Exception\LocalizedException
+ * @deprecated
+ * @see Adapter::createSourceByPath()
*/
public static function factory($type, $directory, $source, $options = null)
{
- if (!is_string($type) || !$type) {
- throw new \Magento\Framework\Exception\LocalizedException(
- __('The adapter type must be a non-empty string.')
- );
- }
- $adapterClass = 'Magento\ImportExport\Model\Import\Source\\' . ucfirst(strtolower($type));
-
- if (!class_exists($adapterClass)) {
- throw new \Magento\Framework\Exception\LocalizedException(
- __('\'%1\' file extension is not supported', $type)
- );
- }
- $adapter = new $adapterClass($source, $directory, $options);
-
- if (!$adapter instanceof AbstractSource) {
- throw new \Magento\Framework\Exception\LocalizedException(
- __('Adapter must be an instance of \Magento\ImportExport\Model\Import\AbstractSource')
- );
- }
- return $adapter;
+ return self::createBackwardCompatibleInstance()->createSourceByPath($source, $options);
}
/**
@@ -58,9 +54,72 @@ public static function factory($type, $directory, $source, $options = null)
* @param mixed $options OPTIONAL Adapter constructor options
*
* @return AbstractSource
+ * @deprecated
+ * @see Adapter::createSourceByPath()
*/
public static function findAdapterFor($source, $directory, $options = null)
{
- return self::factory(pathinfo($source, PATHINFO_EXTENSION), $directory, $source, $options);
+ return self::createBackwardCompatibleInstance()->createSourceByPath($source, $options);
+ }
+
+
+
+ /**
+ * Finds source for import by file path
+ *
+ * @param $path
+ * @param $options
+ *
+ * @return AbstractSource
+ */
+ public function createSourceByPath($path, $options = [])
+ {
+ try {
+ $options = $this->mapBackwardCompatibleFileParserOption($options);
+ $parser = $this->fileSourceFactory->createFromFilePath($path, $options);
+ } catch (UnsupportedPathException $e) {
+ $this->throwUnsupportedFileException($path);
+ } catch (CorruptedFileException $e) {
+ $this->throwUnsupportedFileException($path);
+ }
+
+ return $parser;
+ }
+
+
+ private function extractFileExtension($path)
+ {
+ $fileName = basename($path);
+
+ if (strpos($path, '.') === false) {
+ return $fileName;
+ }
+
+ $fileExtension = substr($fileName, strrpos($fileName, '.') + 1);
+ return $fileExtension;
+ }
+
+ private function throwUnsupportedFileException($path)
+ {
+ $fileExtension = $this->extractFileExtension($path);
+ throw new \Magento\Framework\Exception\LocalizedException(
+ __('\'%1\' file extension is not supported', $fileExtension)
+ );
+ }
+
+ private function mapBackwardCompatibleFileParserOption($options)
+ {
+ if (!is_array($options)) {
+ $options = ['delimiter' => $options ?? ','];
+ }
+ return $options;
+ }
+
+ /**
+ * @return self
+ */
+ private static function createBackwardCompatibleInstance()
+ {
+ return ObjectManager::getInstance()->create(self::class);
}
}
diff --git a/app/code/Magento/ImportExport/Model/Import/Source/Csv.php b/app/code/Magento/ImportExport/Model/Import/Source/Csv.php
index ad95ca4d45cc4..41b926ae8c7f3 100644
--- a/app/code/Magento/ImportExport/Model/Import/Source/Csv.php
+++ b/app/code/Magento/ImportExport/Model/Import/Source/Csv.php
@@ -7,6 +7,8 @@
/**
* CSV import adapter
+ *
+ * @deprecated
*/
class Csv extends \Magento\ImportExport\Model\Import\AbstractSource
{
diff --git a/app/code/Magento/ImportExport/Model/Import/Source/File.php b/app/code/Magento/ImportExport/Model/Import/Source/File.php
new file mode 100644
index 0000000000000..af0d3c7a3c5b4
--- /dev/null
+++ b/app/code/Magento/ImportExport/Model/Import/Source/File.php
@@ -0,0 +1,31 @@
+parser = $parser;
+ parent::__construct($this->parser->getColumnNames());
+ }
+
+ protected function _getNextRow()
+ {
+ return $this->parser->fetchRow();
+ }
+
+ public function rewind()
+ {
+ $this->parser->reset();
+ parent::rewind();
+ }
+}
diff --git a/app/code/Magento/ImportExport/Model/Import/Source/FileFactory.php b/app/code/Magento/ImportExport/Model/Import/Source/FileFactory.php
new file mode 100644
index 0000000000000..868271fe53625
--- /dev/null
+++ b/app/code/Magento/ImportExport/Model/Import/Source/FileFactory.php
@@ -0,0 +1,53 @@
+parserFactory = $parserFactory;
+ $this->objectManager = $objectManager;
+ }
+
+ /**
+ * Creates file source from file path
+ *
+ * @param string $path
+ * @param array $options
+ * @return AbstractSource
+ */
+ public function createFromFilePath($path, array $options = [])
+ {
+ return $this->createFromFileParser(
+ $this->parserFactory->create($path, $options)
+ );
+ }
+
+ /**
+ * Creates file source from file parser
+ *
+ * @param ParserInterface $parser
+ * @return AbstractSource
+ */
+ public function createFromFileParser(ParserInterface $parser)
+ {
+ return $this->objectManager->create(File::class, ['parser' => $parser]);
+ }
+}
diff --git a/app/code/Magento/ImportExport/Model/Import/Source/FileParser/CompositeParserFactory.php b/app/code/Magento/ImportExport/Model/Import/Source/FileParser/CompositeParserFactory.php
new file mode 100644
index 0000000000000..ef53135166bb8
--- /dev/null
+++ b/app/code/Magento/ImportExport/Model/Import/Source/FileParser/CompositeParserFactory.php
@@ -0,0 +1,62 @@
+parserFactories = [];
+
+ foreach ($parserFactories as $parserFactory) {
+ $this->addParserFactory($parserFactory);
+ }
+ }
+
+ public function create($path, array $options = [])
+ {
+ foreach ($this->parserFactories as $parserFactory) {
+ $parser = $this->createParserIfSupported($parserFactory, $path, $options);
+
+ if ($parser === false) {
+ continue;
+ }
+
+ return $parser;
+ }
+
+ $this->thereWasNoParserFound($path);
+ }
+
+ public function addParserFactory(ParserFactoryInterface $parserFactory)
+ {
+ $this->parserFactories[] = $parserFactory;
+ }
+
+ private function thereWasNoParserFound($path)
+ {
+ throw new UnsupportedPathException($path);
+ }
+
+ private function createParserIfSupported(ParserFactoryInterface $parserFactory, $path, array $options)
+ {
+ try {
+ $parser = $parserFactory->create($path, $options);
+ } catch (UnsupportedPathException $e) {
+ return false;
+ }
+
+ return $parser;
+ }
+}
diff --git a/app/code/Magento/ImportExport/Model/Import/Source/FileParser/CorruptedFileException.php b/app/code/Magento/ImportExport/Model/Import/Source/FileParser/CorruptedFileException.php
new file mode 100644
index 0000000000000..6c91267849421
--- /dev/null
+++ b/app/code/Magento/ImportExport/Model/Import/Source/FileParser/CorruptedFileException.php
@@ -0,0 +1,24 @@
+fileName = $fileName;
+ parent::__construct($message ?: sprintf('File "%s" is corrupted', $fileName));
+ }
+
+
+ public function getFileName()
+ {
+ return $this->fileName;
+ }
+}
diff --git a/app/code/Magento/ImportExport/Model/Import/Source/FileParser/CsvParser.php b/app/code/Magento/ImportExport/Model/Import/Source/FileParser/CsvParser.php
new file mode 100644
index 0000000000000..9d277f3d3ac28
--- /dev/null
+++ b/app/code/Magento/ImportExport/Model/Import/Source/FileParser/CsvParser.php
@@ -0,0 +1,113 @@
+ ',',
+ 'enclosure' => '"',
+ 'escape' => '\\',
+ 'null' => null
+ ];
+
+ /**
+ * File reader
+ *
+ * @var Filesystem\File\ReadInterface
+ */
+ private $file;
+
+ /**
+ * Columns of CSV file
+ *
+ * @var string[]
+ */
+ private $columns;
+
+ public function __construct(
+ Filesystem\File\ReadInterface $file,
+ $options = []
+ ) {
+ $this->file = $file;
+ $this->options = $options + $this->options;
+ $this->columns = $this->fetchCsvLine();
+
+ if ($this->columns === false) {
+ throw new \InvalidArgumentException('CSV file should contain at least 1 row');
+ }
+ }
+
+ public function getColumnNames()
+ {
+ return $this->columns;
+ }
+
+ public function fetchRow()
+ {
+ $row = $this->fetchCsvLine();
+
+ if ($row === false) {
+ return false;
+ }
+
+ return $this->mapRowData($row);
+ }
+
+ public function reset()
+ {
+ $this->file->seek(0);
+ $this->fetchCsvLine();
+ }
+
+ private function fetchCsvLine()
+ {
+ return $this->file->readCsv(
+ 0,
+ $this->options['delimiter'],
+ $this->options['enclosure'],
+ $this->options['escape']
+ );
+ }
+
+ private function mapRowData($row)
+ {
+ $result = [];
+ foreach ($this->columns as $index => $column) {
+ $result[] = $this->mapNullValue(
+ $this->extractRowValue($row, $index)
+ );
+ }
+ return $result;
+ }
+
+ public function __destruct()
+ {
+ $this->file->close();
+ }
+
+ private function mapNullValue($value)
+ {
+ if ($value === $this->options['null']) {
+ $value = null;
+ }
+ return $value;
+ }
+
+ private function extractRowValue($row, $index)
+ {
+ return (isset($row[$index]) ? $row[$index] : '');
+ }
+}
diff --git a/app/code/Magento/ImportExport/Model/Import/Source/FileParser/CsvParserFactory.php b/app/code/Magento/ImportExport/Model/Import/Source/FileParser/CsvParserFactory.php
new file mode 100644
index 0000000000000..5e2a6296904dd
--- /dev/null
+++ b/app/code/Magento/ImportExport/Model/Import/Source/FileParser/CsvParserFactory.php
@@ -0,0 +1,59 @@
+filesystem = $filesystem;
+ $this->objectManager = $objectManager;
+ }
+
+ public function create($filePath, array $options = [])
+ {
+ $directoryCode = $options['directory_code'] ?? DirectoryList::ROOT;
+
+ if (substr($filePath, -4) !== '.csv') {
+ throw new UnsupportedPathException($filePath);
+ }
+
+ $directory = $this->filesystem->getDirectoryRead($directoryCode);
+ $filePath = $directory->getRelativePath($filePath);
+
+ if (!$directory->isFile($filePath)) {
+ throw new \InvalidArgumentException(sprintf('File "%s" does not exists', $filePath));
+ }
+
+ return $this->objectManager->create(
+ CsvParser::class,
+ [
+ 'file' => $directory->openFile($filePath),
+ 'options' => $options
+ ]
+ );
+ }
+}
diff --git a/app/code/Magento/ImportExport/Model/Import/Source/FileParser/ParserFactoryInterface.php b/app/code/Magento/ImportExport/Model/Import/Source/FileParser/ParserFactoryInterface.php
new file mode 100644
index 0000000000000..6f0a5340392fd
--- /dev/null
+++ b/app/code/Magento/ImportExport/Model/Import/Source/FileParser/ParserFactoryInterface.php
@@ -0,0 +1,24 @@
+path = $path;
+ }
+
+ public function getPath()
+ {
+ return $this->path;
+ }
+}
diff --git a/app/code/Magento/ImportExport/Model/Import/Source/FileParser/ZipParserFactory.php b/app/code/Magento/ImportExport/Model/Import/Source/FileParser/ZipParserFactory.php
new file mode 100644
index 0000000000000..503ac87d257ce
--- /dev/null
+++ b/app/code/Magento/ImportExport/Model/Import/Source/FileParser/ZipParserFactory.php
@@ -0,0 +1,118 @@
+filesystem = $filesystem;
+ $this->parserFactory = $parserFactory;
+ $this->isZipAvailable = $isZipAvailable ?? extension_loaded('zip');
+ }
+
+ public function create($path, array $options = [])
+ {
+ $this->assertZipFileIsParsable($path);
+
+ $sourceDirectory = $this->filesystem->getDirectoryRead(DirectoryList::ROOT);
+ $writeDirectory = $this->filesystem->getDirectoryWrite(DirectoryList::TMP);
+
+ $path = $sourceDirectory->getRelativePath($path);
+ $this->assertFileExistance($path, $sourceDirectory);
+
+ $zip = new \ZipArchive();
+
+ if ($zip->open($sourceDirectory->getAbsolutePath($path)) !== true) {
+ throw new CorruptedFileException($path);
+ }
+
+ $files = $this->fetchFileNamesFromZipFile($zip);
+
+ foreach ($files as $file) {
+ $destinationFilename = $this->extractFileIntoDirectory($zip, $writeDirectory, $file);
+
+ try {
+ $parser = $this->parserFactory->create(
+ $destinationFilename,
+ ['directory_code' => DirectoryList::TMP] + $options
+ );
+ } catch (UnsupportedPathException $e) {
+ continue;
+ } finally {
+ $writeDirectory->delete($destinationFilename);
+ }
+
+ return $parser;
+ }
+
+ throw new UnsupportedPathException($path);
+ }
+
+ private function assertZipFileIsParsable($path)
+ {
+ if (substr($path, -4) !== '.zip') {
+ throw new UnsupportedPathException($path);
+ }
+
+ if (!$this->isZipAvailable) {
+ throw new UnsupportedPathException($path, 'Zip extension is not available');
+ }
+ }
+
+ private function assertFileExistance($path, $directory)
+ {
+ if (!$directory->isFile($path)) {
+ throw new UnsupportedPathException($path);
+ }
+ }
+
+ private function fetchFileNamesFromZipFile(\ZipArchive $zip)
+ {
+ $files = [];
+
+ for ($fileIndex = 0; $fileIndex < $zip->numFiles; $fileIndex++) {
+ $fileName = $zip->getNameIndex($fileIndex);
+ if ($this->isDirectoryZipEntry($fileName) || !$fileName) {
+ continue;
+ }
+
+ $files[] = $fileName;
+ }
+
+ return $files;
+ }
+
+ private function isDirectoryZipEntry($fileName)
+ {
+ return substr($fileName, -1) === '/';
+ }
+
+ private function extractFileIntoDirectory(
+ \ZipArchive $zip,
+ Filesystem\Directory\WriteInterface $writeDirectory,
+ $fileName
+ ) {
+ $destinationFilename = 'tmp-' . uniqid() . '-' . basename($fileName);
+
+ $destinationFileWriter = $writeDirectory->openFile($destinationFilename);
+ $destinationFileWriter->write($zip->getFromName($fileName));
+ $destinationFileWriter->close();
+
+ return $destinationFilename;
+ }
+}
diff --git a/app/code/Magento/ImportExport/Model/Import/Source/Zip.php b/app/code/Magento/ImportExport/Model/Import/Source/Zip.php
index b7fafc43ca4ed..ff92e035cb0da 100644
--- a/app/code/Magento/ImportExport/Model/Import/Source/Zip.php
+++ b/app/code/Magento/ImportExport/Model/Import/Source/Zip.php
@@ -7,6 +7,8 @@
/**
* Zip import adapter.
+ *
+ * @deprecated
*/
class Zip extends Csv
{
diff --git a/app/code/Magento/ImportExport/Test/Unit/Model/Import/AdapterTest.php b/app/code/Magento/ImportExport/Test/Unit/Model/Import/AdapterTest.php
index 63a7d3c2078b6..b01f9df54f631 100644
--- a/app/code/Magento/ImportExport/Test/Unit/Model/Import/AdapterTest.php
+++ b/app/code/Magento/ImportExport/Test/Unit/Model/Import/AdapterTest.php
@@ -5,33 +5,157 @@
*/
namespace Magento\ImportExport\Test\Unit\Model\Import;
-use Magento\ImportExport\Model\Import\Adapter as Adapter;
+use Magento\Framework\App\Filesystem\DirectoryList;
+use Magento\Framework\Exception\LocalizedException;
+use Magento\Framework\Filesystem;
+use Magento\ImportExport\Model\Import\Adapter;
+use Magento\ImportExport\Model\Import\Source\FileFactory;
+use Magento\ImportExport\Model\Import\Source\FileParser\CsvParserFactory;
+use Magento\ImportExport\Model\Import\Source\FileParser\ParserFactoryInterface;
+use Magento\ImportExport\Model\Import\Source\FileParser\ZipParserFactory;
+use Magento\ImportExport\Test\Unit\Model\Import\Source\FileParser\FakeObjectManager;
+use Magento\ImportExport\Test\Unit\Model\Import\Source\FileParser\FakeParser;
+use Magento\ImportExport\Test\Unit\Model\Import\Source\FileParser\FakeParserFactory;
class AdapterTest extends \PHPUnit_Framework_TestCase
{
- /**
- * @var Adapter|\PHPUnit_Framework_MockObject_MockObject
- */
- protected $adapter;
+ public function testFactory()
+ {
+ $this->markTestSkipped('Skipped because factory method has static modifier');
+ }
- protected function setUp()
+ public function testFindAdapterFor()
+ {
+ $this->markTestSkipped('Skipped because findAdapterFor method has static modifier');
+ }
+
+ public function testWhenFileIsNotSupported_FileExtensionRelatedExceptionIsThrown()
{
- $this->adapter = $this->getMock(
- \Magento\ImportExport\Model\Import\Adapter::class,
- [],
- [],
- '',
- false
+ $adapter = new Adapter(
+ $this->createFileFactory()
);
+
+ $this->setExpectedException(LocalizedException::class, '\'xyz\' file extension is not supported');
+
+ $adapter->createSourceByPath('file.xyz');
}
- public function testFactory()
+ public function testWhenFileIsNotSupportedAndFileNameHasNoExtension_LocalizedExceptionWithBasenameIsThrown()
{
- $this->markTestSkipped('Skipped because factory method has static modifier');
+ $adapter = new Adapter(
+ $this->createFileFactory()
+ );
+
+ $this->setExpectedException(LocalizedException::class, '\'file\' file extension is not supported');
+
+ $adapter->createSourceByPath('file');
}
- public function testFindAdapterFor()
+ public function testWhenFileAdapterIsNotSupported_FileExtensionRelatedExceptionIsThrown()
{
- $this->markTestSkipped('Skipped because findAdapterFor method has static modifier');
+ $adapter = new Adapter(
+ $this->createFileFactory(
+ new ZipParserFactory(
+ $this->createTestFilesystem(),
+ new FakeParserFactory()
+ )
+ )
+ );
+
+ $this->setExpectedException(LocalizedException::class, '\'zip\' file extension is not supported');
+
+ $adapter->createSourceByPath('corrupted.zip');
+ }
+
+ public function testWhenParserIsAvailable_FileSourceIsReturned()
+ {
+ $adapter = new Adapter(
+ $this->createFileFactory(
+ new FakeParserFactory(
+ new FakeParser(['column1', 'column2'])
+ )
+ )
+ );
+
+ $source = $adapter->createSourceByPath('test.csv');
+ $this->assertSame(['column1', 'column2'], $source->getColNames());
+ }
+
+ public function testWhenParserOptionsAreProvided_FileSourceIsReadCorrectly()
+ {
+ $adapter = new Adapter(
+ $this->createFileFactory(
+ new CsvParserFactory(
+ $this->createTestFilesystem(),
+ new FakeObjectManager()
+ )
+ )
+ );
+
+ $source = $adapter->createSourceByPath(
+ 'test_options.csv',
+ [
+ 'delimiter' => '|',
+ 'enclosure' => ';'
+ ]
+ );
+
+ $this->assertSame(['column1', 'column2', 'column3'], $source->getColNames());
+ }
+
+ public function testWhenParserOptionIsProvidedAsString_FileSourceIsReadCorrectly()
+ {
+ $adapter = new Adapter(
+ $this->createFileFactory(
+ new CsvParserFactory(
+ $this->createTestFilesystem(),
+ new FakeObjectManager()
+ )
+ )
+ );
+
+ $source = $adapter->createSourceByPath(
+ 'test_options.csv',
+ '|'
+ );
+
+ $this->assertSame(['column1', ';column2;', 'column3'], $source->getColNames());
+ }
+
+ public function testWhenParserOptionIsProvidedAsNull_FileSourceIsReadCorrectly()
+ {
+ $adapter = new Adapter(
+ $this->createFileFactory(
+ new CsvParserFactory(
+ $this->createTestFilesystem(),
+ new FakeObjectManager()
+ )
+ )
+ );
+
+ $source = $adapter->createSourceByPath(
+ 'test.csv',
+ null
+ );
+
+ $this->assertSame(['column1', 'column2', 'column3'], $source->getColNames());
+ }
+
+
+ private function createTestFilesystem()
+ {
+ return new Filesystem(
+ new DirectoryList(__DIR__ . '/Source/FileParser/_files'),
+ new Filesystem\Directory\ReadFactory(new Filesystem\DriverPool()),
+ new Filesystem\Directory\WriteFactory(new Filesystem\DriverPool())
+ );
+ }
+
+ private function createFileFactory(ParserFactoryInterface $parserFactory = null)
+ {
+ return new FileFactory(
+ $parserFactory ?? new FakeParserFactory(),
+ new FakeObjectManager()
+ );
}
}
diff --git a/app/code/Magento/ImportExport/Test/Unit/Model/Import/Source/FileFactoryTest.php b/app/code/Magento/ImportExport/Test/Unit/Model/Import/Source/FileFactoryTest.php
new file mode 100644
index 0000000000000..03268bc027993
--- /dev/null
+++ b/app/code/Magento/ImportExport/Test/Unit/Model/Import/Source/FileFactoryTest.php
@@ -0,0 +1,77 @@
+setExpectedException(UnsupportedPathException::class, 'Path "test.csv" is not supported');
+
+ $fileFactory = new FileFactory(
+ new FakeParserFactory(),
+ new FakeObjectManager()
+ );
+
+ $fileFactory->createFromFilePath('test.csv');
+ }
+
+ public function testGivenParserFactoryConfiguredWhenSourceIsCreatedByPathThenRightInstanceIsCreated()
+ {
+ $fileFactory = new FileFactory(
+ new FakeParserFactory(new FakeParser(['column1', 'column2'])),
+ new FakeObjectManager()
+ );
+
+ $this->assertSame(
+ ['column1', 'column2'],
+ $fileFactory->createFromFilePath('test.csv')->getColNames()
+ );
+ }
+
+ public function testWhenSourceIsCreatedByPathWithCsvOptionsThenOptionsArePassedToParser()
+ {
+ $objectManager = new FakeObjectManager();
+ $filesystem = new Filesystem(
+ new DirectoryList(__DIR__ . '/FileParser/_files'),
+ new Filesystem\Directory\ReadFactory(new Filesystem\DriverPool()),
+ new Filesystem\Directory\WriteFactory(new Filesystem\DriverPool())
+ );
+
+ $fileFactory = new FileFactory(
+ new CsvParserFactory(
+ $filesystem,
+ $objectManager
+ ),
+ $objectManager
+ );
+
+ $file = $fileFactory->createFromFilePath('test_options.csv', ['delimiter' => '|', 'enclosure' => ';']);
+
+ $this->assertSame(
+ ['column1', 'column2', 'column3'],
+ $file->getColNames()
+ );
+ }
+
+ public function testWhenSourceIsCreatedWithParserThenFileIsCreated()
+ {
+ $fileFactory = new FileFactory(new FakeParserFactory(), new FakeObjectManager());
+ $file = $fileFactory->createFromFileParser(new FakeParser(['column1', 'column2']));
+ $this->assertSame(['column1', 'column2'], $file->getColNames());
+ }
+}
diff --git a/app/code/Magento/ImportExport/Test/Unit/Model/Import/Source/FileParser/CompositeParserFactoryTest.php b/app/code/Magento/ImportExport/Test/Unit/Model/Import/Source/FileParser/CompositeParserFactoryTest.php
new file mode 100644
index 0000000000000..fff6800712061
--- /dev/null
+++ b/app/code/Magento/ImportExport/Test/Unit/Model/Import/Source/FileParser/CompositeParserFactoryTest.php
@@ -0,0 +1,62 @@
+setExpectedException(UnsupportedPathException::class, 'Path "file.csv" is not supported');
+
+ $compositeFactory = new CompositeParserFactory();
+ $compositeFactory->create('file.csv');
+ }
+
+ public function testWhenPathIsNotSupportedByAnyFactoryThenNoParserIsCreated()
+ {
+ $this->setExpectedException(UnsupportedPathException::class, 'Path "file.zip" is not supported');
+
+ $compositeFactory = new CompositeParserFactory();
+ $compositeFactory->addParserFactory(new FakeParserFactory(['file.csv' => new FakeParser()]));
+ $compositeFactory->create('file.zip');
+ }
+
+ public function testWhenPathIsSupportedByFirstFactoryThenParserIsReturned()
+ {
+ $expectedParser = new FakeParser();
+
+ $compositeFactory = new CompositeParserFactory();
+ $compositeFactory->addParserFactory(new FakeParserFactory($expectedParser));
+ $compositeFactory->addParserFactory(new FakeParserFactory(new FakeParser()));
+
+ $this->assertSame($expectedParser, $compositeFactory->create('file.csv'));
+ }
+
+ public function testWhenPathIsSupportedBySecondFactoryThenParserIsReturned()
+ {
+ $expectedParser = new FakeParser();
+
+ $compositeFactory = new CompositeParserFactory();
+ $compositeFactory->addParserFactory(new FakeParserFactory());
+ $compositeFactory->addParserFactory(new FakeParserFactory(['file.csv' => $expectedParser]));
+
+ $this->assertSame($expectedParser, $compositeFactory->create('file.csv'));
+ }
+
+
+ public function testWhenFactoryIsProvidedViaConstructorThenParserIsReturned()
+ {
+ $expectedParser = new FakeParser();
+
+ $compositeFactory = new CompositeParserFactory([new FakeParserFactory($expectedParser)]);
+
+ $this->assertSame($expectedParser, $compositeFactory->create('file.csv'));
+ }
+}
diff --git a/app/code/Magento/ImportExport/Test/Unit/Model/Import/Source/FileParser/CorruptedPathExceptionTest.php b/app/code/Magento/ImportExport/Test/Unit/Model/Import/Source/FileParser/CorruptedPathExceptionTest.php
new file mode 100644
index 0000000000000..abcb4a5a897b5
--- /dev/null
+++ b/app/code/Magento/ImportExport/Test/Unit/Model/Import/Source/FileParser/CorruptedPathExceptionTest.php
@@ -0,0 +1,40 @@
+assertEmpty($exception->getFileName());
+ }
+
+ public function testWhenFileNameIsProvidedThenFileNameCanBeRetrievedLater()
+ {
+ $exception = new CorruptedFileException('file.csv');
+
+ $this->assertSame('file.csv', $exception->getFileName());
+ }
+
+ public function testWhenMessageIsProvidedThenMessageCanBeRetrievedLater()
+ {
+ $exception = new CorruptedFileException('', 'My message');
+
+ $this->assertSame('My message', $exception->getMessage());
+ }
+
+ public function testWhenNoMessageIsProvidedThenMessageIsGeneratedFromPath()
+ {
+ $exception = new CorruptedFileException('file.csv');
+
+ $this->assertSame('File "file.csv" is corrupted', $exception->getMessage());
+ }
+}
diff --git a/app/code/Magento/ImportExport/Test/Unit/Model/Import/Source/FileParser/CsvParserFactoryTest.php b/app/code/Magento/ImportExport/Test/Unit/Model/Import/Source/FileParser/CsvParserFactoryTest.php
new file mode 100644
index 0000000000000..17023016800b3
--- /dev/null
+++ b/app/code/Magento/ImportExport/Test/Unit/Model/Import/Source/FileParser/CsvParserFactoryTest.php
@@ -0,0 +1,114 @@
+setExpectedException(FileParser\UnsupportedPathException::class);
+
+ $factory = $this->createCsvParserFactory();
+ $factory->create('test.zip');
+ }
+
+ public function testWhenFileDoesNotExistThenNoParserIsCreated()
+ {
+ $this->setExpectedException(
+ \InvalidArgumentException::class,
+ 'File "non_existing_file.csv" does not exists'
+ );
+
+ $factory = $this->createCsvParserFactory();
+ $factory->create('non_existing_file.csv');
+ }
+
+ public function testWhenFileIsNotAccesibleThenNoParserIsCreated()
+ {
+ $this->setExpectedException(
+ \InvalidArgumentException::class,
+ 'File "test.csv" does not exists'
+ );
+
+ $factory = $this->createCsvParserFactory(tempnam(sys_get_temp_dir(), 'non_created'));
+ $factory->create('test.csv');
+ }
+
+ public function testWhenValidFileIsProvidedThenParserIsCreated()
+ {
+ $factory = $this->createCsvParserFactory();
+
+ $this->assertCsvFile(
+ ['column1', 'column2', 'column3'],
+ $factory->create('test.csv')
+ );
+ }
+
+ public function testWhenAbsolutePathIsProvidedThenParserIsCreated()
+ {
+ $factory = $this->createCsvParserFactory();
+
+ $this->assertCsvFile(
+ ['column1', 'column2', 'column3'],
+ $factory->create(__DIR__ . '/_files/test.csv')
+ );
+ }
+
+ public function testWhenCustomDirectoryIsProvidedThenParserIsCreatedFromIt()
+ {
+ $factory = $this->createCsvParserFactory();
+
+ $this->assertCsvFile(
+ ['column1', 'column2'],
+ $factory->create('test.csv', ['directory_code' => DirectoryList::TMP])
+ );
+ }
+
+ public function testWhenCustomCSVOptionsProvidedThenParserIsCreatedFromIt()
+ {
+ $factory = $this->createCsvParserFactory();
+
+ $this->assertCsvFile(
+ ['column1', 'column2', 'column3'],
+ $factory->create(
+ 'test_options.csv',
+ [
+ 'delimiter' => '|',
+ 'enclosure' => ';'
+ ]
+ )
+ );
+ }
+
+ private function createTestFilesystem($baseDirectory = null)
+ {
+ $baseDirectory = $baseDirectory ?? __DIR__ . '/_files';
+
+ return new Filesystem(
+ new DirectoryList($baseDirectory),
+ new Filesystem\Directory\ReadFactory(new Filesystem\DriverPool()),
+ new Filesystem\Directory\WriteFactory(new Filesystem\DriverPool())
+ );
+ }
+
+ private function createCsvParserFactory($baseDirectory = null)
+ {
+ return new FileParser\CsvParserFactory(
+ $this->createTestFilesystem($baseDirectory),
+ new FakeObjectManager()
+ );
+ }
+
+ private function assertCsvFile($expectedColumns, FileParser\CsvParser $csvParser)
+ {
+ $this->assertSame($expectedColumns, $csvParser->getColumnNames());
+ }
+}
diff --git a/app/code/Magento/ImportExport/Test/Unit/Model/Import/Source/FileParser/CsvParserTest.php b/app/code/Magento/ImportExport/Test/Unit/Model/Import/Source/FileParser/CsvParserTest.php
new file mode 100644
index 0000000000000..768485edca3af
--- /dev/null
+++ b/app/code/Magento/ImportExport/Test/Unit/Model/Import/Source/FileParser/CsvParserTest.php
@@ -0,0 +1,175 @@
+setExpectedException(\InvalidArgumentException::class, 'CSV file should contain at least 1 row');
+
+ $this->createParser(new FakeFile([]));
+ }
+
+ public function testWhenValidFileIsProvidedThenReadColumnsFromFileHeader()
+ {
+ $parser = $this->createParser(new FakeFile([
+ 'column1,column2,column3',
+ 'row1value1,row1value2,row1value3',
+ ]));
+
+ $this->assertSame(['column1', 'column2', 'column3'], $parser->getColumnNames());
+ }
+
+ public function testWhenValidFileIsProvidedThenRowsAreFetchedAndHeaderIsSkipped()
+ {
+ $parser = $this->createParser(new FakeFile([
+ 'column1,column2,column3',
+ 'row1value1,row1value2,row1value3',
+ 'row2value1,row2value2,row2value3',
+ ]));
+
+ $this->assertParsedFileContent(
+ [
+ ['row1value1', 'row1value2', 'row1value3'],
+ ['row2value1', 'row2value2', 'row2value3']
+ ],
+ $parser
+ );
+ }
+
+ public function testWhenEndOfFileIsReachedThenNoMoreRowsCanBeFetched()
+ {
+ $parser = $this->createParser(new FakeFile([
+ 'column1,column2,column3',
+ 'row1value1,row1value2,row1value3',
+ 'row2value1,row2value2,row2value3',
+ ]));
+
+ $this->skipRows(2, $parser);
+
+ $this->assertFalse($parser->fetchRow());
+ }
+
+ public function testWhenFileWasAlreadyReadThenResetAllowsToReadItFromStart()
+ {
+ $parser = $this->createParser(new FakeFile([
+ 'column1,column2,column3',
+ 'row1value1,row1value2,row1value3',
+ 'row2value1,row2value2,row2value3',
+ ]));
+
+ $this->skipRows(2, $parser);
+ $parser->reset();
+
+ $this->assertParsedFileContent(
+ [
+ ['row1value1', 'row1value2', 'row1value3'],
+ ['row2value1', 'row2value2', 'row2value3']
+ ],
+ $parser
+ );
+ }
+
+ public function testWhenCustomDelimiterIsSpecifiedThenDataIsParsedUsingThisDelimiter()
+ {
+ $parser = $this->createParser(
+ new FakeFile([
+ 'column1|column2|column3',
+ 'row1value1|row1value2|row1value3',
+ 'row2value1|row2value2|row2value3'
+ ]),
+ ['delimiter' => '|']
+ );
+
+ $this->assertParsedFileContent(
+ [
+ ['row1value1', 'row1value2', 'row1value3'],
+ ['row2value1', 'row2value2', 'row2value3']
+ ],
+ $parser
+ );
+ }
+
+ public function testWhenFileHasMissingRowValuesThenFetchedRowValuesAreSameAsNumberOfColumns()
+ {
+ $parser = $this->createParser(new FakeFile([
+ 'column1,column2,column3',
+ 'row1value1',
+ 'row2value1,row2value2,row2value3',
+ 'row3value1,row3value2',
+ 'row4value1,row4value2,row4value3,row4value4',
+ ]));
+
+ $this->assertParsedFileContent(
+ [
+ ['row1value1', '', ''],
+ ['row2value1', 'row2value2', 'row2value3'],
+ ['row3value1', 'row3value2', ''],
+ ['row4value1', 'row4value2', 'row4value3']
+ ],
+ $parser
+ );
+ }
+
+
+ public function testWhenNullPlaceholderIsSetThenFetchedRowValueIsReplacedWithNull()
+ {
+ $parser = $this->createParser(
+ new FakeFile([
+ 'column1,column2,column3',
+ 'row1value1,row1value2,row1value3',
+ 'row2value1,row2value2,row2value3',
+ ]),
+ ['null' => 'row2value2']
+ );
+
+ $this->assertParsedFileContent(
+ [
+ ['row1value1', 'row1value2', 'row1value3'],
+ ['row2value1', null, 'row2value3']
+ ],
+ $parser
+ );
+ }
+
+ public function testWhenParserIsDestroyedThenInternalFileDescriptorIsClosed()
+ {
+ $csvFile = new FakeFile([
+ 'column1,column2,column3'
+ ]);
+
+ $parser = $this->createParser($csvFile);
+ unset($parser);
+
+ $this->assertFalse($csvFile->isOpen());
+ }
+
+ private function assertParsedFileContent($expectedCsvStructure, $parser)
+ {
+ $actualCsvStructure = [];
+ while ($row = $parser->fetchRow()) {
+ $actualCsvStructure[] = $row;
+ }
+
+ $this->assertSame($expectedCsvStructure, $actualCsvStructure);
+ }
+
+ private function skipRows($numberOfRows, FileParser\CsvParser $parser)
+ {
+ for ($i = 0; $i < $numberOfRows; $i++) {
+ $parser->fetchRow();
+ }
+ }
+
+ private function createParser($file, $options = [])
+ {
+ return new FileParser\CsvParser($file, $options);
+ }
+}
diff --git a/app/code/Magento/ImportExport/Test/Unit/Model/Import/Source/FileParser/FakeFile.php b/app/code/Magento/ImportExport/Test/Unit/Model/Import/Source/FileParser/FakeFile.php
new file mode 100644
index 0000000000000..f7abcc14d672f
--- /dev/null
+++ b/app/code/Magento/ImportExport/Test/Unit/Model/Import/Source/FileParser/FakeFile.php
@@ -0,0 +1,89 @@
+lines = $lines;
+ $this->pointer = 0;
+ $this->isOpen = true;
+ }
+
+ public function isOpen()
+ {
+ return $this->isOpen;
+ }
+
+ public function readCsv($length = 0, $delimiter = ',', $enclosure = '"', $escape = '\\')
+ {
+ if ($this->isEndOfFile()) {
+ return false;
+ }
+
+ return str_getcsv($this->readFileLine($length), $delimiter, $enclosure, $escape);
+ }
+
+ private function readFileLine($length)
+ {
+ return $this->truncateLineToLength($this->lines[$this->pointer++], $length);
+ }
+
+ private function isEndOfFile()
+ {
+ return !isset($this->lines[$this->pointer]);
+ }
+
+ private function truncateLineToLength($line, $length)
+ {
+ if ($length > 0 && strlen($line) > $length) {
+ $line = substr($line, 0, $length);
+ }
+ return $line;
+ }
+ public function close()
+ {
+ $this->isOpen = false;
+ }
+
+ public function read($length)
+ {
+ return '';
+ }
+
+ public function readLine($length, $ending = null)
+ {
+ return $this->readFileLine($length);
+ }
+
+ public function tell()
+ {
+ return $this->pointer;
+ }
+
+ public function seek($length, $whence = SEEK_SET)
+ {
+ $this->pointer = $length;
+ }
+
+ public function eof()
+ {
+ return $this->isEndOfFile();
+ }
+
+ public function stat()
+ {
+ return [];
+ }
+}
diff --git a/app/code/Magento/ImportExport/Test/Unit/Model/Import/Source/FileParser/FakeObjectManager.php b/app/code/Magento/ImportExport/Test/Unit/Model/Import/Source/FileParser/FakeObjectManager.php
new file mode 100644
index 0000000000000..537db6d710f9a
--- /dev/null
+++ b/app/code/Magento/ImportExport/Test/Unit/Model/Import/Source/FileParser/FakeObjectManager.php
@@ -0,0 +1,61 @@
+resolveConstructorArguments($type, $arguments);
+ return new $type(...$arguments);
+ }
+
+ public function get($type)
+ {
+ if (!isset($this->instances[$type])) {
+ $this->instances[$type] = $this->create($type);
+ }
+
+ return $this->instances[$type];
+ }
+
+ public function configure(array $configuration)
+ {
+ // Fake object manager is not configurable
+ }
+
+ private function resolveConstructorArguments($type, array $arguments)
+ {
+ $constructorSignature = new \ReflectionMethod($type, '__construct');
+ $resolvedArguments = [];
+ foreach ($constructorSignature->getParameters() as $parameter) {
+ $arguments = $this->assertParameterValue($arguments, $parameter);
+
+ $resolvedArguments[] = $arguments[$parameter->getName()] ?? $parameter->getDefaultValue();
+ }
+
+ return $resolvedArguments;
+ }
+
+ private function assertParameterValue(array $arguments, $parameter): array
+ {
+ if (!isset($arguments[$parameter->getName()]) && !$parameter->isDefaultValueAvailable()) {
+ new \RuntimeException(
+ sprintf(
+ 'Cannot instantiate %s without default value for constructor argument $%s',
+ $parameter->getDeclaringClass()->getName(),
+ $parameter->getName()
+ )
+ );
+ }
+ return $arguments;
+ }
+}
diff --git a/app/code/Magento/ImportExport/Test/Unit/Model/Import/Source/FileParser/FakeParser.php b/app/code/Magento/ImportExport/Test/Unit/Model/Import/Source/FileParser/FakeParser.php
new file mode 100644
index 0000000000000..f27e4d988470c
--- /dev/null
+++ b/app/code/Magento/ImportExport/Test/Unit/Model/Import/Source/FileParser/FakeParser.php
@@ -0,0 +1,48 @@
+columns = $columns;
+ $this->rows = $rows;
+ $this->position = 0;
+ }
+
+
+ public function getColumnNames()
+ {
+ return $this->columns;
+ }
+
+ public function fetchRow()
+ {
+ if ($this->isEndOfRows()) {
+ return false;
+ }
+
+ return $this->rows[$this->position++];
+ }
+
+ public function reset()
+ {
+ $this->position = 0;
+ }
+
+ private function isEndOfRows()
+ {
+ return !isset($this->rows[$this->position]);
+ }
+}
diff --git a/app/code/Magento/ImportExport/Test/Unit/Model/Import/Source/FileParser/FakeParserFactory.php b/app/code/Magento/ImportExport/Test/Unit/Model/Import/Source/FileParser/FakeParserFactory.php
new file mode 100644
index 0000000000000..4f86c88ab3663
--- /dev/null
+++ b/app/code/Magento/ImportExport/Test/Unit/Model/Import/Source/FileParser/FakeParserFactory.php
@@ -0,0 +1,54 @@
+parser = $parser;
+ }
+
+ public function create($path, array $options = [])
+ {
+ if ($this->isParserMap()) {
+ return $this->findParserInMap($path);
+ }
+
+ return $this->parser;
+ }
+
+ private function findParserInMap($path)
+ {
+ $path = $this->trimTemporaryFilePrefix($path);
+
+ if (!isset($this->parser[$path])) {
+ throw new UnsupportedPathException($path);
+ }
+
+ return $this->parser[$path];
+ }
+
+ private function isParserMap()
+ {
+ return is_array($this->parser);
+ }
+
+ private function trimTemporaryFilePrefix($path)
+ {
+ if (strpos($path, 'tmp-') === 0) {
+ $path = preg_replace('/tmp-[a-z0-9]+-/i', '', $path);
+ }
+
+ return $path;
+ }
+}
diff --git a/app/code/Magento/ImportExport/Test/Unit/Model/Import/Source/FileParser/UnsupportedPathExceptionTest.php b/app/code/Magento/ImportExport/Test/Unit/Model/Import/Source/FileParser/UnsupportedPathExceptionTest.php
new file mode 100644
index 0000000000000..c6a05d0f47888
--- /dev/null
+++ b/app/code/Magento/ImportExport/Test/Unit/Model/Import/Source/FileParser/UnsupportedPathExceptionTest.php
@@ -0,0 +1,40 @@
+assertEmpty($exception->getPath());
+ }
+
+ public function testWhenFileNameIsProvidedThenFileNameCanBeRetrievedLater()
+ {
+ $exception = new UnsupportedPathException('file.csv');
+
+ $this->assertSame('file.csv', $exception->getPath());
+ }
+
+ public function testWhenMessageIsProvidedThenMessageCanBeRetrievedLater()
+ {
+ $exception = new UnsupportedPathException('', 'My message');
+
+ $this->assertSame('My message', $exception->getMessage());
+ }
+
+ public function testWhenNoMessageIsProvidedThenMessageIsGeneratedFromPath()
+ {
+ $exception = new UnsupportedPathException('file.csv');
+
+ $this->assertSame('Path "file.csv" is not supported', $exception->getMessage());
+ }
+}
diff --git a/app/code/Magento/ImportExport/Test/Unit/Model/Import/Source/FileParser/ZipParserFactoryTest.php b/app/code/Magento/ImportExport/Test/Unit/Model/Import/Source/FileParser/ZipParserFactoryTest.php
new file mode 100644
index 0000000000000..c968a9202c179
--- /dev/null
+++ b/app/code/Magento/ImportExport/Test/Unit/Model/Import/Source/FileParser/ZipParserFactoryTest.php
@@ -0,0 +1,151 @@
+setExpectedException(FileParser\UnsupportedPathException::class, 'Zip extension is not available');
+
+ $parser = new FileParser\ZipParserFactory(
+ $this->createTestFilesystem(),
+ new FakeParserFactory(),
+ false
+ );
+
+ $parser->create('file.zip');
+ }
+
+ public function testWhenCsvFileIsProvidedThenParserIsNotCreated()
+ {
+ $this->setExpectedException(FileParser\UnsupportedPathException::class, 'Path "file.csv" is not supported');
+
+ $parser = $this->createZipParserFactory();
+ $parser->create('file.csv');
+ }
+
+ public function testWhenCorruptedZipFileIsProvidedThenParserIsNotCreated()
+ {
+ $this->setExpectedException(FileParser\CorruptedFileException::class);
+
+ $parser = $this->createZipParserFactory();
+ $parser->create('corrupted.zip');
+ }
+
+ public function testWhenZipFileDoesNotExistsThenParserIsNotCreated()
+ {
+ $this->setExpectedException(FileParser\UnsupportedPathException::class, 'Path "unknown.zip" is not supported');
+
+ $parser = $this->createZipParserFactory();
+ $parser->create('unknown.zip');
+ }
+
+
+ public function testWhenEmptyZipFileIsProvidedThenParserIsNotCreated()
+ {
+ $this->setExpectedException(FileParser\UnsupportedPathException::class, 'Path "empty.zip" is not supported');
+
+ $parserFactory = $this->createZipParserFactory();
+ $parserFactory->create('empty.zip');
+ }
+
+ public function testWhenProperZipFileIsProvidedThenFirstFileIsParsed()
+ {
+ $expectedParser = new FakeParser();
+
+ $parserFactory = new FileParser\ZipParserFactory(
+ $this->createTestFilesystem(),
+ new FakeParserFactory([
+ 'test.csv' => $expectedParser
+ ])
+ );
+
+ $this->assertSame(
+ $expectedParser,
+ $parserFactory->create('complete.zip')
+ );
+ }
+
+ public function testWhenProperZipFileWithAbsolutePathIsProvidedThenFirstFileIsParsed()
+ {
+ $expectedParser = new FakeParser();
+
+ $parserFactory = $this->createZipParserFactory(
+ new FakeParserFactory([
+ 'test.csv' => $expectedParser
+ ])
+ );
+
+ $this->assertSame(
+ $expectedParser,
+ $parserFactory->create(__DIR__ . '/_files/complete.zip')
+ );
+ }
+
+ public function testWhenProperZipFileIsProvidedThenSecondFileIsParsed()
+ {
+ $expectedParser = new FakeParser();
+
+ $parserFactory = $this->createZipParserFactory(
+ new FakeParserFactory([
+ 'test.tsv' => $expectedParser
+ ])
+ );
+
+ $this->assertSame(
+ $expectedParser,
+ $parserFactory->create('complete.zip')
+ );
+ }
+
+ public function testWhenProperZipFileIsProvidedWithOptionsThenCustomCsvFileIsParsed()
+ {
+ $parserFactory = $this->createZipParserFactory(
+ new FileParser\CsvParserFactory(
+ $this->createTestFilesystem(),
+ new FakeObjectManager()
+ )
+ );
+
+ $parser = $parserFactory->create(
+ 'custom_option.zip',
+ [
+ 'delimiter' => '|',
+ 'enclosure' => ';'
+ ]
+ );
+
+ $this->assertSame(
+ ['column1', 'column2', 'column3'],
+ $parser->getColumnNames()
+ );
+ }
+
+ private function createTestFilesystem()
+ {
+ return new Filesystem(
+ new DirectoryList(__DIR__ . '/_files'),
+ new Filesystem\Directory\ReadFactory(new Filesystem\DriverPool()),
+ new Filesystem\Directory\WriteFactory(new Filesystem\DriverPool())
+ );
+ }
+
+ private function createZipParserFactory($parserFactory = null): FileParser\ZipParserFactory
+ {
+ $parser = new FileParser\ZipParserFactory(
+ $this->createTestFilesystem(),
+ $parserFactory ?: new FakeParserFactory()
+ );
+
+ return $parser;
+ }
+}
diff --git a/app/code/Magento/ImportExport/Test/Unit/Model/Import/Source/FileParser/_files/complete.zip b/app/code/Magento/ImportExport/Test/Unit/Model/Import/Source/FileParser/_files/complete.zip
new file mode 100644
index 0000000000000..b4c17bb771e8a
Binary files /dev/null and b/app/code/Magento/ImportExport/Test/Unit/Model/Import/Source/FileParser/_files/complete.zip differ
diff --git a/app/code/Magento/ImportExport/Test/Unit/Model/Import/Source/FileParser/_files/corrupted.zip b/app/code/Magento/ImportExport/Test/Unit/Model/Import/Source/FileParser/_files/corrupted.zip
new file mode 100644
index 0000000000000..567e7db20f304
Binary files /dev/null and b/app/code/Magento/ImportExport/Test/Unit/Model/Import/Source/FileParser/_files/corrupted.zip differ
diff --git a/app/code/Magento/ImportExport/Test/Unit/Model/Import/Source/FileParser/_files/custom_option.zip b/app/code/Magento/ImportExport/Test/Unit/Model/Import/Source/FileParser/_files/custom_option.zip
new file mode 100644
index 0000000000000..98f29d6425ce8
Binary files /dev/null and b/app/code/Magento/ImportExport/Test/Unit/Model/Import/Source/FileParser/_files/custom_option.zip differ
diff --git a/app/code/Magento/ImportExport/Test/Unit/Model/Import/Source/FileParser/_files/empty.zip b/app/code/Magento/ImportExport/Test/Unit/Model/Import/Source/FileParser/_files/empty.zip
new file mode 100644
index 0000000000000..15cb0ecb3e219
Binary files /dev/null and b/app/code/Magento/ImportExport/Test/Unit/Model/Import/Source/FileParser/_files/empty.zip differ
diff --git a/app/code/Magento/ImportExport/Test/Unit/Model/Import/Source/FileParser/_files/test.csv b/app/code/Magento/ImportExport/Test/Unit/Model/Import/Source/FileParser/_files/test.csv
new file mode 100644
index 0000000000000..3acca666172ea
--- /dev/null
+++ b/app/code/Magento/ImportExport/Test/Unit/Model/Import/Source/FileParser/_files/test.csv
@@ -0,0 +1 @@
+column1,column2,column3
diff --git a/app/code/Magento/ImportExport/Test/Unit/Model/Import/Source/FileParser/_files/test_options.csv b/app/code/Magento/ImportExport/Test/Unit/Model/Import/Source/FileParser/_files/test_options.csv
new file mode 100644
index 0000000000000..e1a3763d1e399
--- /dev/null
+++ b/app/code/Magento/ImportExport/Test/Unit/Model/Import/Source/FileParser/_files/test_options.csv
@@ -0,0 +1 @@
+column1|;column2;|column3
diff --git a/app/code/Magento/ImportExport/Test/Unit/Model/Import/Source/FileParser/_files/var/tmp/test.csv b/app/code/Magento/ImportExport/Test/Unit/Model/Import/Source/FileParser/_files/var/tmp/test.csv
new file mode 100644
index 0000000000000..18288adde1528
--- /dev/null
+++ b/app/code/Magento/ImportExport/Test/Unit/Model/Import/Source/FileParser/_files/var/tmp/test.csv
@@ -0,0 +1 @@
+column1,column2
diff --git a/app/code/Magento/ImportExport/Test/Unit/Model/Import/Source/FileTest.php b/app/code/Magento/ImportExport/Test/Unit/Model/Import/Source/FileTest.php
new file mode 100644
index 0000000000000..2c9ca0641cb14
--- /dev/null
+++ b/app/code/Magento/ImportExport/Test/Unit/Model/Import/Source/FileTest.php
@@ -0,0 +1,110 @@
+assertSame(['column1', 'column2'], $file->getColNames());
+ }
+
+ public function testWhenEmptyFileParserIsProvidedThenIteratorIsNotValid()
+ {
+ $file = new File(new FakeParser(
+ ['column1', 'column2']
+ ));
+ $file->rewind();
+ $this->assertFalse($file->valid());
+ }
+
+ public function testWhenSomeDataInFileParserIsProvidedThenIteratorIsValid()
+ {
+ $file = new File(new FakeParser(
+ ['column1', 'column2'],
+ [
+ ['value1', 'value2']
+ ]
+ ));
+
+ $file->rewind();
+ $this->assertTrue($file->valid());
+ }
+
+ public function testWhenSomeDataInFileParserIsProvidedThenIteratorRowIsReturned()
+ {
+ $file = new File(new FakeParser(
+ ['column1', 'column2'],
+ [
+ ['value1', 'value2']
+ ]
+ ));
+
+ $file->rewind();
+
+ $this->assertSame(
+ [
+ 'column1' => 'value1',
+ 'column2' => 'value2'
+ ],
+ $file->current()
+ );
+ }
+
+ public function testWhenSpecificPositionIsSetThenProperIteratorRowIsReturned()
+ {
+ $file = new File(new FakeParser(
+ ['column1', 'column2'],
+ [
+ ['wrong', 'wrong'],
+ ['correct1', 'correct2'],
+ ['wrong', 'wrong'],
+ ]
+ ));
+
+ $file->rewind();
+ $file->seek(1);
+
+ $this->assertSame(
+ [
+ 'column1' => 'correct1',
+ 'column2' => 'correct2'
+ ],
+ $file->current()
+ );
+ }
+
+ public function testWhenIteratorIsRewindedThenParserRestarts()
+ {
+ $file = new File(new FakeParser(
+ ['column1', 'column2'],
+ [
+ ['value1.1', 'value2.1'],
+ ['value1.2', 'value2.2'],
+ ['value1.3', 'value2.3']
+ ]
+ ));
+
+ $file->rewind();
+ $file->next();
+ $file->next();
+ $file->rewind();
+
+ $this->assertSame(
+ [
+ 'column1' => 'value1.1',
+ 'column2' => 'value2.1'
+ ],
+ $file->current()
+ );
+ }
+}
diff --git a/app/code/Magento/ImportExport/Test/Unit/Model/Import/_files/test.csv b/app/code/Magento/ImportExport/Test/Unit/Model/Import/_files/test.csv
new file mode 100644
index 0000000000000..ef071b0dab0bf
--- /dev/null
+++ b/app/code/Magento/ImportExport/Test/Unit/Model/Import/_files/test.csv
@@ -0,0 +1,4 @@
+column1,"column2"
+value1,value2
+3,4
+5
diff --git a/app/code/Magento/ImportExport/etc/di.xml b/app/code/Magento/ImportExport/etc/di.xml
index 47acf7a356d93..9b8d959514dde 100644
--- a/app/code/Magento/ImportExport/etc/di.xml
+++ b/app/code/Magento/ImportExport/etc/di.xml
@@ -10,6 +10,8 @@
+
+
@@ -17,4 +19,15 @@
+
+
+
+
+ - Magento\ImportExport\Model\Import\Source\FileParser\CsvParserFactory
+
+ - Magento\ImportExport\Model\Import\Source\FileParser\ZipParserFactory\Proxy
+
+
+
+