diff --git a/CHANGELOG.md b/CHANGELOG.md index c06cec1..fca165d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,12 +4,14 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com), and this project adheres to [Semantic Versioning](https://semver.org). -## [Unreleased] +## [2.2.0] - 2021-02-06 ### Added * [#21](https://github.com/shlinkio/shlink-importer/issues/21) Added support to import URL `title` prop. +* [#5](https://github.com/shlinkio/shlink-importer/issues/5) Added support to import from a standard CSV file. ### Changed * Migrated build to Github Actions. +* [#23](https://github.com/shlinkio/shlink-importer/issues/23) Increased required MSI to 75%. ### Deprecated * *Nothing* diff --git a/README.md b/README.md index be9f328..0569e75 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,15 @@ This module can be installed using composer: ## Supported import sources -* Bit.ly +#### Bit.ly + +It imports using the API v4. The only required param is an [access token](https://bitly.is/accesstoken). + +#### Standard CSV + +It parses a CSV file with the `Long URL` and `Short code` columns. It can optionally contain `Domain`, `Title` and `Tags`, being the latter a pipe-separated list of items (`foo|bar|baz`). + +Column names can have spaces and have any combination of upper and lowercase. ## Usage diff --git a/composer.json b/composer.json index 8e034d9..f194e8e 100644 --- a/composer.json +++ b/composer.json @@ -14,10 +14,11 @@ "require": { "php": "^7.4 || ^8.0", "ext-json": "*", - "laminas/laminas-servicemanager": "^3.4", - "lstrojny/functional-php": "^1.14", + "laminas/laminas-servicemanager": "^3.6.4", + "league/csv": "^9.6", + "lstrojny/functional-php": "^1.15", "shlinkio/shlink-config": "^1.0", - "symfony/console": "^5.1" + "symfony/console": "^5.2" }, "require-dev": { "guzzlehttp/guzzle": "^7.2", @@ -57,7 +58,7 @@ "test": "phpdbg -qrr vendor/bin/phpunit --order-by=random --testdox --colors=always", "test:ci": "@test --coverage-clover=build/clover.xml --coverage-xml=build/coverage-xml --log-junit=build/junit.xml", "test:pretty": "@test --coverage-html build/coverage-html", - "infect": "infection --threads=4 --min-msi=70 --log-verbosity=default --only-covered", + "infect": "infection --threads=4 --min-msi=75 --log-verbosity=default --only-covered", "infect:ci": "@infect --coverage=build --skip-initial-tests", "infect:show": "@infect --show-mutations", "infect:show:ci": "@infect --show-mutations --coverage=build", diff --git a/config/dependencies.config.php b/config/dependencies.config.php index 27a5be9..56f1f87 100644 --- a/config/dependencies.config.php +++ b/config/dependencies.config.php @@ -33,27 +33,31 @@ 'cli' => [ 'importer_strategies' => [ 'factories' => [ - Strategy\BitlyApiImporter::class => ConfigAbstractFactory::class, + Sources\Bitly\BitlyApiImporter::class => ConfigAbstractFactory::class, + Sources\Csv\CsvImporter::class => InvokableFactory::class, ], 'aliases' => [ - Strategy\ImportSources::BITLY => Strategy\BitlyApiImporter::class, + Sources\ImportSources::BITLY => Sources\Bitly\BitlyApiImporter::class, + Sources\ImportSources::CSV => Sources\Csv\CsvImporter::class, ], ], 'params_console_helpers' => [ 'factories' => [ - Params\ConsoleHelper\BitlyApiParamsConsoleHelper::class => InvokableFactory::class, + Sources\Bitly\BitlyApiParamsConsoleHelper::class => InvokableFactory::class, + Sources\Csv\CsvParamsConsoleHelper::class => InvokableFactory::class, ], 'aliases' => [ - Strategy\ImportSources::BITLY => Params\ConsoleHelper\BitlyApiParamsConsoleHelper::class, + Sources\ImportSources::BITLY => Sources\Bitly\BitlyApiParamsConsoleHelper::class, + Sources\ImportSources::CSV => Sources\Csv\CsvParamsConsoleHelper::class, ], ], ], ConfigAbstractFactory::class => [ - Strategy\BitlyApiImporter::class => [ClientInterface::class, RequestFactoryInterface::class], + Sources\Bitly\BitlyApiImporter::class => [ClientInterface::class, RequestFactoryInterface::class], Command\ImportCommand::class => [ Strategy\ImporterStrategyManager::class, Params\ConsoleHelper\ConsoleHelperManager::class, diff --git a/src/Command/ImportCommand.php b/src/Command/ImportCommand.php index d845c99..f61afe5 100644 --- a/src/Command/ImportCommand.php +++ b/src/Command/ImportCommand.php @@ -9,9 +9,9 @@ use Shlinkio\Shlink\Importer\ImportedLinksProcessorInterface; use Shlinkio\Shlink\Importer\Params\ConsoleHelper\ConsoleHelperManagerInterface; use Shlinkio\Shlink\Importer\Params\ConsoleHelper\ParamsConsoleHelperInterface; +use Shlinkio\Shlink\Importer\Sources\ImportSources; use Shlinkio\Shlink\Importer\Strategy\ImporterStrategyInterface; use Shlinkio\Shlink\Importer\Strategy\ImporterStrategyManagerInterface; -use Shlinkio\Shlink\Importer\Strategy\ImportSources; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; @@ -47,8 +47,8 @@ protected function configure(): void ->setName(self::NAME) ->setDescription('Allows to import short URLs from third party sources') ->addArgument('source', InputArgument::REQUIRED, sprintf( - 'The source from which you want to import. Supported sources: ["%s"]', - implode('", "', ImportSources::getAll()), + 'The source from which you want to import. Supported sources: [%s]', + implode(', ', ImportSources::getAll()), )); } diff --git a/src/Exception/InvalidSourceException.php b/src/Exception/InvalidSourceException.php index 724830e..d551dae 100644 --- a/src/Exception/InvalidSourceException.php +++ b/src/Exception/InvalidSourceException.php @@ -5,7 +5,7 @@ namespace Shlinkio\Shlink\Importer\Exception; use InvalidArgumentException; -use Shlinkio\Shlink\Importer\Strategy\ImportSources; +use Shlinkio\Shlink\Importer\Sources\ImportSources; use function implode; use function sprintf; diff --git a/src/Exception/BitlyApiException.php b/src/Sources/Bitly/BitlyApiException.php similarity index 82% rename from src/Exception/BitlyApiException.php rename to src/Sources/Bitly/BitlyApiException.php index 9e70df2..40d7f1b 100644 --- a/src/Exception/BitlyApiException.php +++ b/src/Sources/Bitly/BitlyApiException.php @@ -2,7 +2,9 @@ declare(strict_types=1); -namespace Shlinkio\Shlink\Importer\Exception; +namespace Shlinkio\Shlink\Importer\Sources\Bitly; + +use Shlinkio\Shlink\Importer\Exception\ImportException; use function sprintf; diff --git a/src/Strategy/BitlyApiImporter.php b/src/Sources/Bitly/BitlyApiImporter.php similarity index 93% rename from src/Strategy/BitlyApiImporter.php rename to src/Sources/Bitly/BitlyApiImporter.php index 388397c..0381c91 100644 --- a/src/Strategy/BitlyApiImporter.php +++ b/src/Sources/Bitly/BitlyApiImporter.php @@ -2,17 +2,19 @@ declare(strict_types=1); -namespace Shlinkio\Shlink\Importer\Strategy; +namespace Shlinkio\Shlink\Importer\Sources\Bitly; use JsonException; use Psr\Http\Client\ClientExceptionInterface; use Psr\Http\Client\ClientInterface; use Psr\Http\Message\RequestFactoryInterface; -use Shlinkio\Shlink\Importer\Exception\BitlyApiException; use Shlinkio\Shlink\Importer\Exception\ImportException; -use Shlinkio\Shlink\Importer\Model\BitlyApiProgressTracker; use Shlinkio\Shlink\Importer\Model\ImportedShlinkUrl; -use Shlinkio\Shlink\Importer\Params\BitlyApiParams; +use Shlinkio\Shlink\Importer\Sources\Bitly\BitlyApiException; +use Shlinkio\Shlink\Importer\Sources\Bitly\BitlyApiParams; +use Shlinkio\Shlink\Importer\Sources\Bitly\BitlyApiProgressTracker; +use Shlinkio\Shlink\Importer\Sources\ImportSources; +use Shlinkio\Shlink\Importer\Strategy\ImporterStrategyInterface; use Shlinkio\Shlink\Importer\Util\DateHelpersTrait; use Throwable; diff --git a/src/Params/BitlyApiParams.php b/src/Sources/Bitly/BitlyApiParams.php similarity index 95% rename from src/Params/BitlyApiParams.php rename to src/Sources/Bitly/BitlyApiParams.php index 09f9098..e9fe892 100644 --- a/src/Params/BitlyApiParams.php +++ b/src/Sources/Bitly/BitlyApiParams.php @@ -2,9 +2,9 @@ declare(strict_types=1); -namespace Shlinkio\Shlink\Importer\Params; +namespace Shlinkio\Shlink\Importer\Sources\Bitly; -class BitlyApiParams +final class BitlyApiParams { private string $accessToken; private bool $importTags; diff --git a/src/Params/ConsoleHelper/BitlyApiParamsConsoleHelper.php b/src/Sources/Bitly/BitlyApiParamsConsoleHelper.php similarity index 92% rename from src/Params/ConsoleHelper/BitlyApiParamsConsoleHelper.php rename to src/Sources/Bitly/BitlyApiParamsConsoleHelper.php index e4047a7..e3a4f75 100644 --- a/src/Params/ConsoleHelper/BitlyApiParamsConsoleHelper.php +++ b/src/Sources/Bitly/BitlyApiParamsConsoleHelper.php @@ -2,8 +2,9 @@ declare(strict_types=1); -namespace Shlinkio\Shlink\Importer\Params\ConsoleHelper; +namespace Shlinkio\Shlink\Importer\Sources\Bitly; +use Shlinkio\Shlink\Importer\Params\ConsoleHelper\ParamsConsoleHelperInterface; use Symfony\Component\Console\Style\StyleInterface; class BitlyApiParamsConsoleHelper implements ParamsConsoleHelperInterface diff --git a/src/Model/BitlyApiProgressTracker.php b/src/Sources/Bitly/BitlyApiProgressTracker.php similarity index 95% rename from src/Model/BitlyApiProgressTracker.php rename to src/Sources/Bitly/BitlyApiProgressTracker.php index 9c84ef3..f370d2c 100644 --- a/src/Model/BitlyApiProgressTracker.php +++ b/src/Sources/Bitly/BitlyApiProgressTracker.php @@ -2,11 +2,11 @@ declare(strict_types=1); -namespace Shlinkio\Shlink\Importer\Model; +namespace Shlinkio\Shlink\Importer\Sources\Bitly; use DateInterval; use DateTimeImmutable; -use Shlinkio\Shlink\Importer\Params\BitlyApiParams; +use Shlinkio\Shlink\Importer\Sources\Bitly\BitlyApiParams; use Shlinkio\Shlink\Importer\Util\DateHelpersTrait; use function base64_decode; diff --git a/src/Sources/Csv/CsvImporter.php b/src/Sources/Csv/CsvImporter.php new file mode 100644 index 0000000..a0e0c79 --- /dev/null +++ b/src/Sources/Csv/CsvImporter.php @@ -0,0 +1,89 @@ +date = $date; + } + + /** + * @return ImportedShlinkUrl[] + * @throws ImportException + */ + public function import(array $rawParams): iterable + { + $params = CsvParams::fromRawParams($rawParams); + $now = $this->date ?? new DateTimeImmutable(); + + $csvReader = Reader::createFromStream($params->stream())->setDelimiter($params->delimiter()) + ->setHeaderOffset(0); + + foreach ($csvReader as $record) { + $record = $this->remapRecordHeaders($record); + + yield new ImportedShlinkUrl( + ImportSources::CSV, + $record['longurl'], + $this->parseTags($record), + $now, + $this->nonEmptyValueOrNull($record, 'domain'), + $record['shortcode'], + $this->nonEmptyValueOrNull($record, 'title'), + ); + } + } + + private function remapRecordHeaders(array $record): array + { + return reduce_left($record, static function ($value, string $index, array $c, array $acc) { + $normalizedKey = strtolower(str_replace(' ', '', $index)); + $acc[$normalizedKey] = $value; + + return $acc; + }, []); + } + + private function nonEmptyValueOrNull(array $record, string $key): ?string + { + $value = $record[$key] ?? null; + if (empty($value)) { + return null; + } + + $trimmedValue = trim($value); + if (empty($trimmedValue)) { + return null; + } + + return $trimmedValue; + } + + private function parseTags(array $record): array + { + return array_filter(explode(self::TAG_SEPARATOR, $this->nonEmptyValueOrNull($record, 'tags') ?? '')); + } +} diff --git a/src/Sources/Csv/CsvParams.php b/src/Sources/Csv/CsvParams.php new file mode 100644 index 0000000..4a2a4ac --- /dev/null +++ b/src/Sources/Csv/CsvParams.php @@ -0,0 +1,38 @@ +delimiter = $params['delimiter'] ?? ''; + $instance->stream = $params['stream'] ?? ''; + + return $instance; + } + + /** + * @return resource + */ + public function stream() + { + return $this->stream; + } + + public function delimiter(): string + { + return $this->delimiter; + } +} diff --git a/src/Sources/Csv/CsvParamsConsoleHelper.php b/src/Sources/Csv/CsvParamsConsoleHelper.php new file mode 100644 index 0000000..0205092 --- /dev/null +++ b/src/Sources/Csv/CsvParamsConsoleHelper.php @@ -0,0 +1,42 @@ + true, + 'stream' => $io->ask('What\'s the path for the CSV file you want to import', null, [$this, 'pathToStream']), + 'delimiter' => $io->choice('What\'s the delimiter used to separate values?', [ + ',' => 'Comma', + ';' => 'Semicolon', + ], ','), + ]; + } + + /** + * @return resource + */ + public function pathToStream(?string $value) + { + if (empty($value)) { + throw InvalidPathException::pathNotProvided(); + } + + $file = @fopen($value, 'rb'); + if (! $file) { + throw InvalidPathException::pathIsNotFile($value); + } + + return $file; + } +} diff --git a/src/Sources/Csv/InvalidPathException.php b/src/Sources/Csv/InvalidPathException.php new file mode 100644 index 0000000..2d7e576 --- /dev/null +++ b/src/Sources/Csv/InvalidPathException.php @@ -0,0 +1,23 @@ + [ diff --git a/test/Exception/InvalidSourceExceptionTest.php b/test/Exception/InvalidSourceExceptionTest.php index 19e641e..c02fd46 100644 --- a/test/Exception/InvalidSourceExceptionTest.php +++ b/test/Exception/InvalidSourceExceptionTest.php @@ -21,8 +21,8 @@ public function expectedMessageIsGenerated(string $source, string $expectedMessa public function provideInvalidSources(): iterable { - yield 'foo' => ['foo', 'Provided source "foo" is not valid. Expected one of ["bitly"]']; - yield 'bar' => ['bar', 'Provided source "bar" is not valid. Expected one of ["bitly"]']; - yield 'baz' => ['baz', 'Provided source "baz" is not valid. Expected one of ["bitly"]']; + yield 'foo' => ['foo', 'Provided source "foo" is not valid. Expected one of ["bitly", "csv"]']; + yield 'bar' => ['bar', 'Provided source "bar" is not valid. Expected one of ["bitly", "csv"]']; + yield 'baz' => ['baz', 'Provided source "baz" is not valid. Expected one of ["bitly", "csv"]']; } } diff --git a/test/Exception/BitlyApiExceptionTest.php b/test/Sources/Bitly/BitlyApiExceptionTest.php similarity index 86% rename from test/Exception/BitlyApiExceptionTest.php rename to test/Sources/Bitly/BitlyApiExceptionTest.php index 6fe4d42..92c0481 100644 --- a/test/Exception/BitlyApiExceptionTest.php +++ b/test/Sources/Bitly/BitlyApiExceptionTest.php @@ -2,10 +2,10 @@ declare(strict_types=1); -namespace ShlinkioTest\Shlink\Importer\Exception; +namespace ShlinkioTest\Shlink\Importer\Sources\Bitly; use PHPUnit\Framework\TestCase; -use Shlinkio\Shlink\Importer\Exception\BitlyApiException; +use Shlinkio\Shlink\Importer\Sources\Bitly\BitlyApiException; class BitlyApiExceptionTest extends TestCase { diff --git a/test/Strategy/BitlyApiImporterTest.php b/test/Sources/Bitly/BitlyApiImporterTest.php similarity index 98% rename from test/Strategy/BitlyApiImporterTest.php rename to test/Sources/Bitly/BitlyApiImporterTest.php index 51b5cfd..10d82e7 100644 --- a/test/Strategy/BitlyApiImporterTest.php +++ b/test/Sources/Bitly/BitlyApiImporterTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace ShlinkioTest\Shlink\Importer\Strategy; +namespace ShlinkioTest\Shlink\Importer\Sources\Bitly; use DateTimeImmutable; use DateTimeInterface; @@ -15,10 +15,10 @@ use Psr\Http\Client\ClientInterface; use Psr\Http\Message\RequestFactoryInterface; use Psr\Http\Message\RequestInterface; -use Shlinkio\Shlink\Importer\Exception\BitlyApiException; use Shlinkio\Shlink\Importer\Model\ImportedShlinkUrl as ShlinkUrl; -use Shlinkio\Shlink\Importer\Strategy\BitlyApiImporter; -use Shlinkio\Shlink\Importer\Strategy\ImportSources; +use Shlinkio\Shlink\Importer\Sources\Bitly\BitlyApiException; +use Shlinkio\Shlink\Importer\Sources\Bitly\BitlyApiImporter; +use Shlinkio\Shlink\Importer\Sources\ImportSources; use function explode; use function json_encode; diff --git a/test/Params/ConsoleHelper/BitlyApiParamsConsoleHelperTest.php b/test/Sources/Bitly/BitlyApiParamsConsoleHelperTest.php similarity index 95% rename from test/Params/ConsoleHelper/BitlyApiParamsConsoleHelperTest.php rename to test/Sources/Bitly/BitlyApiParamsConsoleHelperTest.php index 997e0c0..4c617cd 100644 --- a/test/Params/ConsoleHelper/BitlyApiParamsConsoleHelperTest.php +++ b/test/Sources/Bitly/BitlyApiParamsConsoleHelperTest.php @@ -2,13 +2,13 @@ declare(strict_types=1); -namespace ShlinkioTest\Shlink\Importer\Params\ConsoleHelper; +namespace ShlinkioTest\Shlink\Importer\Sources\Bitly; use PHPUnit\Framework\TestCase; use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; use Prophecy\Prophecy\ObjectProphecy; -use Shlinkio\Shlink\Importer\Params\ConsoleHelper\BitlyApiParamsConsoleHelper; +use Shlinkio\Shlink\Importer\Sources\Bitly\BitlyApiParamsConsoleHelper; use Symfony\Component\Console\Style\StyleInterface; use function count; diff --git a/test/Params/BitlyApiParamsTest.php b/test/Sources/Bitly/BitlyApiParamsTest.php similarity index 96% rename from test/Params/BitlyApiParamsTest.php rename to test/Sources/Bitly/BitlyApiParamsTest.php index 4db89e2..b61c91f 100644 --- a/test/Params/BitlyApiParamsTest.php +++ b/test/Sources/Bitly/BitlyApiParamsTest.php @@ -2,10 +2,10 @@ declare(strict_types=1); -namespace ShlinkioTest\Shlink\Importer\Model; +namespace ShlinkioTest\Shlink\Importer\Sources\Bitly; use PHPUnit\Framework\TestCase; -use Shlinkio\Shlink\Importer\Params\BitlyApiParams; +use Shlinkio\Shlink\Importer\Sources\Bitly\BitlyApiParams; class BitlyApiParamsTest extends TestCase { diff --git a/test/Model/BitlyApiProgressTrackerTest.php b/test/Sources/Bitly/BitlyApiProgressTrackerTest.php similarity index 88% rename from test/Model/BitlyApiProgressTrackerTest.php rename to test/Sources/Bitly/BitlyApiProgressTrackerTest.php index 1f31c8c..b248ce5 100644 --- a/test/Model/BitlyApiProgressTrackerTest.php +++ b/test/Sources/Bitly/BitlyApiProgressTrackerTest.php @@ -2,12 +2,12 @@ declare(strict_types=1); -namespace ShlinkioTest\Shlink\Importer\Model; +namespace ShlinkioTest\Shlink\Importer\Sources\Bitly; use DateInterval; use PHPUnit\Framework\TestCase; -use Shlinkio\Shlink\Importer\Model\BitlyApiProgressTracker; -use Shlinkio\Shlink\Importer\Params\BitlyApiParams; +use Shlinkio\Shlink\Importer\Sources\Bitly\BitlyApiParams; +use Shlinkio\Shlink\Importer\Sources\Bitly\BitlyApiProgressTracker; use Shlinkio\Shlink\Importer\Util\DateHelpersTrait; use function base64_encode; diff --git a/test/Sources/Csv/CsvImporterTest.php b/test/Sources/Csv/CsvImporterTest.php new file mode 100644 index 0000000..1114b6e --- /dev/null +++ b/test/Sources/Csv/CsvImporterTest.php @@ -0,0 +1,132 @@ +importer = new CsvImporter($this->getDate()); + } + + /** + * @test + * @dataProvider provideCSVs + */ + public function csvIsProperlyImported(string $csv, string $delimiter, array $expectedList): void + { + $rawOptions = ['delimiter' => $delimiter, 'stream' => $this->createCsvStream($csv)]; + + $result = $this->importer->import($rawOptions); + + $urls = []; + foreach ($result as $item) { + $urls[] = $item; + } + + self::assertEquals($expectedList, $urls); + } + + public function provideCSVs(): iterable + { + yield 'comma separator' => [ + <<getDate(), + null, + '123', + null, + ), + new ImportedShlinkUrl( + ImportSources::CSV, + 'https://facebook.com', + [], + $this->getDate(), + 'example.com', + '456', + 'my title', + ), + ], + ]; + yield 'semicolon separator' => [ + <<getDate(), + null, + 'abc', + null, + ), + new ImportedShlinkUrl( + ImportSources::CSV, + 'https://facebook.com', + ['foo', 'baz'], + $this->getDate(), + 'example.com', + 'def', + null, + ), + new ImportedShlinkUrl( + ImportSources::CSV, + 'https://shlink.io/documentation', + [], + $this->getDate(), + 'example.com', + 'ghi', + 'the title', + ), + ], + ]; + } + + /** + * @return resource + */ + private function createCsvStream(string $csv) + { + $stream = fopen('php://memory', 'rb+'); + fwrite($stream, $csv); + rewind($stream); + + return $stream; + } + + private function getDate(): DateTimeInterface + { + static $date; + return $date ?? ($date = new DateTimeImmutable()); + } +} diff --git a/test/Sources/Csv/CsvParamsConsoleHelperTest.php b/test/Sources/Csv/CsvParamsConsoleHelperTest.php new file mode 100644 index 0000000..7ee6910 --- /dev/null +++ b/test/Sources/Csv/CsvParamsConsoleHelperTest.php @@ -0,0 +1,74 @@ +helper = new CsvParamsConsoleHelper(); + $this->io = $this->prophesize(StyleInterface::class); + } + + /** @test */ + public function requestsParams(): void + { + $ask = $this->io->ask(Argument::cetera())->willReturn('stream'); + $choice = $this->io->choice(Argument::cetera())->willReturn(';'); + + $result = $this->helper->requestParams($this->io->reveal()); + + self::assertEquals([ + 'import_short_codes' => true, + 'stream' => 'stream', + 'delimiter' => ';', + ], $result); + $ask->shouldHaveBeenCalledOnce(); + $choice->shouldHaveBeenCalledOnce(); + } + + /** + * @test + * @dataProvider provideEmptyStreamValues + */ + public function pathToStreamThrowsExceptionWithInvalidValue(?string $value, string $expectedMessage): void + { + $this->expectException(InvalidPathException::class); + $this->expectExceptionMessage($expectedMessage); + + $this->helper->pathToStream($value); + } + + public function provideEmptyStreamValues(): iterable + { + yield 'null' => [null, 'The path of the file is required.']; + yield 'empty string' => ['', 'The path of the file is required.']; + yield 'invalid file' => [ + 'this is not a file', + 'The file "this is not a file" does not seem to exist. Try another one.', + ]; + } + + /** @test */ + public function pathIsProperlyParsedToStream(): void + { + $result = $this->helper->pathToStream(__FILE__); + + self::assertIsResource($result); + } +} diff --git a/test/Sources/Csv/InvalidPathExceptionTest.php b/test/Sources/Csv/InvalidPathExceptionTest.php new file mode 100644 index 0000000..90a1824 --- /dev/null +++ b/test/Sources/Csv/InvalidPathExceptionTest.php @@ -0,0 +1,39 @@ +getMessage()); + } + + /** + * @test + * @dataProvider providePaths + */ + public function pathIsNotFileCreatesExceptionAsExpected(string $path): void + { + $e = InvalidPathException::pathIsNotFile($path); + + self::assertEquals(sprintf('The file "%s" does not seem to exist. Try another one.', $path), $e->getMessage()); + } + + public function providePaths(): iterable + { + yield ['/foo']; + yield ['/bar']; + yield ['/bar/baz']; + } +}