From 9e32886f60ff120a4b51d66e6b0f20dd6e205c64 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 13 Feb 2022 12:20:02 +0100 Subject: [PATCH 001/108] Created first CLI E2E tests --- composer.json | 3 + config/test/bootstrap_cli_tests.php | 19 ++++++ config/test/test_config.global.php | 1 + .../test-cli/Command/GenerateApiKeyTest.php | 21 +++++++ .../CLI/test-cli/Command/ListApiKeysTest.php | 63 +++++++++++++++++++ .../Rest/test-api/Fixtures/ApiKeyFixture.php | 2 +- phpunit-cli.xml | 20 ++++++ 7 files changed, 128 insertions(+), 1 deletion(-) create mode 100644 config/test/bootstrap_cli_tests.php create mode 100644 module/CLI/test-cli/Command/GenerateApiKeyTest.php create mode 100644 module/CLI/test-cli/Command/ListApiKeysTest.php create mode 100644 phpunit-cli.xml diff --git a/composer.json b/composer.json index 52ba9c87..cf294d9e 100644 --- a/composer.json +++ b/composer.json @@ -92,6 +92,7 @@ "autoload-dev": { "psr-4": { "ShlinkioTest\\Shlink\\CLI\\": "module/CLI/test", + "ShlinkioCliTest\\Shlink\\CLI\\": "module/CLI/test-cli", "ShlinkioTest\\Shlink\\Rest\\": "module/Rest/test", "ShlinkioApiTest\\Shlink\\Rest\\": "module/Rest/test-api", "ShlinkioTest\\Shlink\\Core\\": "module/Core/test", @@ -138,6 +139,8 @@ "test:db:ms": "DB_DRIVER=mssql composer test:db:sqlite", "test:api": "bin/test/run-api-tests.sh", "test:api:ci": "GENERATE_COVERAGE=yes composer test:api", + "test:cli": "APP_ENV=test DB_DRIVER=maria TEST_ENV=cli php vendor/bin/phpunit --order-by=random --colors=always --testdox -c phpunit-cli.xml", + "test:cli:ci": "GENERATE_COVERAGE=yes composer test:cli", "infect:ci:base": "infection --threads=4 --log-verbosity=default --only-covered --only-covering-test-cases --skip-initial-tests", "infect:ci:unit": "@infect:ci:base --coverage=build/coverage-unit --min-msi=80", "infect:ci:db": "@infect:ci:base --coverage=build/coverage-db --min-msi=95 --configuration=infection-db.json", diff --git a/config/test/bootstrap_cli_tests.php b/config/test/bootstrap_cli_tests.php new file mode 100644 index 00000000..703677d2 --- /dev/null +++ b/config/test/bootstrap_cli_tests.php @@ -0,0 +1,19 @@ +get(Helper\TestHelper::class); +$config = $container->get('config'); +$em = $container->get(EntityManager::class); + +$testHelper->createTestDb(['bin/cli', 'db:create'], ['bin/cli', 'db:migrate']); +CliTest\CliTestCase::setSeedFixturesCallback( + static fn () => $testHelper->seedFixtures($em, $config['data_fixtures'] ?? []), +); diff --git a/config/test/test_config.global.php b/config/test/test_config.global.php index 89807b26..c3151194 100644 --- a/config/test/test_config.global.php +++ b/config/test/test_config.global.php @@ -178,6 +178,7 @@ return [ 'data_fixtures' => [ 'paths' => [ + // TODO These are used for CLI tests too, so maybe should be somewhere else __DIR__ . '/../../module/Rest/test-api/Fixtures', ], ], diff --git a/module/CLI/test-cli/Command/GenerateApiKeyTest.php b/module/CLI/test-cli/Command/GenerateApiKeyTest.php new file mode 100644 index 00000000..b14462c0 --- /dev/null +++ b/module/CLI/test-cli/Command/GenerateApiKeyTest.php @@ -0,0 +1,21 @@ +exec([GenerateKeyCommand::NAME]); + + self::assertStringContainsString('[OK] Generated API key', $output); + self::assertEquals(ExitCodes::EXIT_SUCCESS, $exitCode); + } +} diff --git a/module/CLI/test-cli/Command/ListApiKeysTest.php b/module/CLI/test-cli/Command/ListApiKeysTest.php new file mode 100644 index 00000000..96b60e92 --- /dev/null +++ b/module/CLI/test-cli/Command/ListApiKeysTest.php @@ -0,0 +1,63 @@ +exec([ListKeysCommand::NAME, ...$flags]); + + self::assertEquals($expectedOutput, $output); + self::assertEquals(ExitCodes::EXIT_SUCCESS, $exitCode); + } + + public function provideFlags(): iterable + { + $expiredApiKeyDate = Chronos::now()->subDay()->startOfDay()->toAtomString(); + $enabledOnlyOutput = << [[], << [['-e'], $enabledOnlyOutput]; + yield '--enabled-only' => [['--enabled-only'], $enabledOnlyOutput]; + } +} diff --git a/module/Rest/test-api/Fixtures/ApiKeyFixture.php b/module/Rest/test-api/Fixtures/ApiKeyFixture.php index ef6d1781..54797bb4 100644 --- a/module/Rest/test-api/Fixtures/ApiKeyFixture.php +++ b/module/Rest/test-api/Fixtures/ApiKeyFixture.php @@ -25,7 +25,7 @@ class ApiKeyFixture extends AbstractFixture implements DependentFixtureInterface { $manager->persist($this->buildApiKey('valid_api_key', true)); $manager->persist($this->buildApiKey('disabled_api_key', false)); - $manager->persist($this->buildApiKey('expired_api_key', true, Chronos::now()->subDay())); + $manager->persist($this->buildApiKey('expired_api_key', true, Chronos::now()->subDay()->startOfDay())); $authorApiKey = $this->buildApiKey('author_api_key', true); $authorApiKey->registerRole(RoleDefinition::forAuthoredShortUrls()); diff --git a/phpunit-cli.xml b/phpunit-cli.xml new file mode 100644 index 00000000..49ba781e --- /dev/null +++ b/phpunit-cli.xml @@ -0,0 +1,20 @@ + + + + + ./module/*/test-cli + + + + + + ./module/CLI/src + ./module/Core/src + + + From 1b6512fc8dae9a08d89a155d13603a55a02caf83 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 27 Feb 2022 08:10:18 +0100 Subject: [PATCH 002/108] Replaced deprecated transactional function with wrapTransaction --- module/Core/src/Service/UrlShortener.php | 2 +- module/Core/test/Service/UrlShortenerTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/module/Core/src/Service/UrlShortener.php b/module/Core/src/Service/UrlShortener.php index 8fa54493..ec79858f 100644 --- a/module/Core/src/Service/UrlShortener.php +++ b/module/Core/src/Service/UrlShortener.php @@ -39,7 +39,7 @@ class UrlShortener implements UrlShortenerInterface /** @var ShortUrlMeta $meta */ $meta = $this->titleResolutionHelper->processTitleAndValidateUrl($meta); - return $this->em->transactional(function () use ($meta) { + return $this->em->wrapInTransaction(function () use ($meta) { $shortUrl = ShortUrl::fromMeta($meta, $this->relationResolver); $this->verifyShortCodeUniqueness($meta, $shortUrl); diff --git a/module/Core/test/Service/UrlShortenerTest.php b/module/Core/test/Service/UrlShortenerTest.php index bdd508b4..2fb9b017 100644 --- a/module/Core/test/Service/UrlShortenerTest.php +++ b/module/Core/test/Service/UrlShortenerTest.php @@ -39,7 +39,7 @@ class UrlShortenerTest extends TestCase [$shortUrl] = $arguments; $shortUrl->setId('10'); }); - $this->em->transactional(Argument::type('callable'))->will(function (array $args) { + $this->em->wrapInTransaction(Argument::type('callable'))->will(function (array $args) { /** @var callable $callback */ [$callback] = $args; From 4d082a87a1e8e0a54dbd54a3d91964b9c8ca3418 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 27 Feb 2022 08:11:33 +0100 Subject: [PATCH 003/108] Added preliminary config to export coverage for CLI tests --- composer.json | 4 +- config/test/test_config.global.php | 100 +++++++++++++++++++++++++---- 2 files changed, 91 insertions(+), 13 deletions(-) diff --git a/composer.json b/composer.json index cf294d9e..9f8326e1 100644 --- a/composer.json +++ b/composer.json @@ -61,9 +61,9 @@ "symfony/string": "^6.0" }, "require-dev": { - "cebe/php-openapi": "^1.5", + "cebe/php-openapi": "^1.6", "devster/ubench": "^2.1", - "dms/phpunit-arraysubset-asserts": "^0.3.0", + "dms/phpunit-arraysubset-asserts": "^0.4.0", "infection/infection": "^0.26", "openswoole/ide-helper": "~4.9.1", "phpspec/prophecy-phpunit": "^2.0", diff --git a/config/test/test_config.global.php b/config/test/test_config.global.php index c3151194..838531de 100644 --- a/config/test/test_config.global.php +++ b/config/test/test_config.global.php @@ -8,9 +8,11 @@ use GuzzleHttp\Client; use Laminas\ConfigAggregator\ConfigAggregator; use Laminas\Diactoros\Response\EmptyResponse; use Laminas\ServiceManager\Factory\InvokableFactory; +use League\Event\EventDispatcher; use Monolog\Handler\StreamHandler; use Monolog\Logger; use PHPUnit\Runner\Version; +use Psr\Container\ContainerInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\RequestHandlerInterface; @@ -20,6 +22,10 @@ use SebastianBergmann\CodeCoverage\Filter; use SebastianBergmann\CodeCoverage\Report\Html\Facade as Html; use SebastianBergmann\CodeCoverage\Report\PHP; use SebastianBergmann\CodeCoverage\Report\Xml\Facade as Xml; +use Symfony\Component\Console\Application; +use Symfony\Component\Console\Event\ConsoleCommandEvent; +use Symfony\Component\Console\Event\ConsoleTerminateEvent; +use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; use function Laminas\Stratigility\middleware; use function Shlinkio\Shlink\Config\env; @@ -30,14 +36,39 @@ use const ShlinkioTest\Shlink\SWOOLE_TESTING_HOST; use const ShlinkioTest\Shlink\SWOOLE_TESTING_PORT; $isApiTest = env('TEST_ENV') === 'api'; +$isCliTest = env('TEST_ENV') === 'cli'; +$isE2eTest = $isApiTest || $isCliTest; $generateCoverage = env('GENERATE_COVERAGE') === 'yes'; -if ($isApiTest && $generateCoverage) { + +$coverage = null; +if ($isE2eTest && $generateCoverage) { $filter = new Filter(); $filter->includeDirectory(__DIR__ . '/../../module/Core/src'); - $filter->includeDirectory(__DIR__ . '/../../module/Rest/src'); + $filter->includeDirectory(__DIR__ . '/../../module/' . ($isApiTest ? 'Rest' : 'CLI') . '/src'); $coverage = new CodeCoverage((new Selector())->forLineCoverage($filter), $filter); } +/** + * @param 'api'|'cli' $type + * @param array<'cov'|'xml'|'html'> $formats + */ +$exportCoverage = static function (string $type = 'api', array $formats = ['cov', 'xml', 'html']) use (&$coverage): void { + if ($coverage === null) { + return; + } + + $basePath = __DIR__ . '/../../build/coverage-' . $type; + + foreach ($formats as $format) { + match ($format) { + 'cov' => (new PHP())->process($coverage, $basePath . '.cov'), + 'xml' => (new Xml(Version::getVersionString()))->process($coverage, $basePath . '/coverage-xml'), + 'html' => (new Html())->process($coverage, $basePath . '/coverage-html'), + default => null, + }; + } +}; + $buildDbConnection = static function (): array { $driver = env('DB_DRIVER', 'sqlite'); $isCi = env('CI', false); @@ -119,17 +150,10 @@ return [ [ 'name' => 'dump_coverage', 'path' => '/api-tests/stop-coverage', - 'middleware' => middleware(static function () use (&$coverage) { + 'middleware' => middleware(static function () use ($exportCoverage) { // TODO I have tried moving this block to a listener so that it's invoked automatically, // but then the coverage is generated empty ¯\_(ツ)_/¯ - if ($coverage) { // @phpstan-ignore-line - $basePath = __DIR__ . '/../../build/coverage-api'; - - (new PHP())->process($coverage, $basePath . '.cov'); - (new Xml(Version::getVersionString()))->process($coverage, $basePath . '/coverage-xml'); - (new Html())->process($coverage, $basePath . '/coverage-html'); - } - + $exportCoverage(); return new EmptyResponse(); }), 'allowed_methods' => ['GET'], @@ -170,6 +194,60 @@ return [ 'factories' => [ TestUtils\Helper\TestHelper::class => InvokableFactory::class, ], + 'delegators' => $isCliTest ? [ + Application::class => [ + static function ( + ContainerInterface $c, + string $serviceName, + callable $callback, + ) use ( + &$coverage, + $exportCoverage, + ) { + /** @var Application $app */ + $app = $callback(); + $wrappedEventDispatcher = new EventDispatcher(); + + $wrappedEventDispatcher->subscribeTo( + ConsoleCommandEvent::class, + static function () use (&$coverage): void { + $id = env('COVERAGE_ID'); + if ($id === null) { + return; + } + + $coverage?->start($id); + }, + ); + $wrappedEventDispatcher->subscribeTo( + ConsoleTerminateEvent::class, + static function () use (&$coverage, $exportCoverage): void { + $id = env('COVERAGE_ID'); + if ($id === null) { + return; + } + + $coverage?->stop(); + $exportCoverage('cli'); + }, + ); + + $app->setDispatcher(new class ($wrappedEventDispatcher) implements EventDispatcherInterface { + public function __construct(private EventDispatcher $wrappedDispatcher) + { + } + + public function dispatch(object $event, ?string $eventName = null): object + { + $this->wrappedDispatcher->dispatch($event); + return $event; + } + }); + + return $app; + }, + ], + ] : [], ], 'entity_manager' => [ From db47a9a253bc67debe17e77367d5c8fddba89e51 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Tue, 9 Aug 2022 19:15:49 +0200 Subject: [PATCH 004/108] Added mutation tests for CLI E2E tests --- composer.json | 15 +++++++++++---- infection-cli.json | 24 ++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 4 deletions(-) create mode 100644 infection-cli.json diff --git a/composer.json b/composer.json index 815fc6d6..6a91dd85 100644 --- a/composer.json +++ b/composer.json @@ -57,8 +57,8 @@ }, "require-dev": { "cebe/php-openapi": "^1.7", - "dms/phpunit-arraysubset-asserts": "^0.4.0", "devster/ubench": "^2.1", + "dms/phpunit-arraysubset-asserts": "^0.4.0", "infection/infection": "^0.26.5", "openswoole/ide-helper": "~4.11.1", "phpspec/prophecy-phpunit": "^2.0", @@ -115,12 +115,14 @@ "test": [ "@test:unit", "@test:db", - "@test:api" + "@test:api", + "@test:cli" ], "test:ci": [ "@test:unit:ci", "@test:db", - "@test:api:ci" + "@test:api:ci", + "@test:cli:ci" ], "test:unit": "@php vendor/bin/phpunit --order-by=random --colors=always --coverage-php build/coverage-unit.cov --testdox", "test:unit:ci": "@test:unit --coverage-xml=build/coverage-unit/coverage-xml --log-junit=build/coverage-unit/junit.xml", @@ -135,11 +137,12 @@ "test:api": "bin/test/run-api-tests.sh", "test:api:ci": "GENERATE_COVERAGE=yes composer test:api", "test:cli": "APP_ENV=test DB_DRIVER=maria TEST_ENV=cli php vendor/bin/phpunit --order-by=random --colors=always --testdox -c phpunit-cli.xml", - "test:cli:ci": "GENERATE_COVERAGE=yes composer test:cli", + "test:cli:ci": "GENERATE_COVERAGE=yes composer test:cli -- --log-junit=build/coverage-cli/junit.xml", "infect:ci:base": "infection --threads=4 --log-verbosity=default --only-covered --only-covering-test-cases --skip-initial-tests", "infect:ci:unit": "@infect:ci:base --coverage=build/coverage-unit --min-msi=84", "infect:ci:db": "@infect:ci:base --coverage=build/coverage-db --min-msi=95 --configuration=infection-db.json", "infect:ci:api": "@infect:ci:base --coverage=build/coverage-api --min-msi=80 --configuration=infection-api.json", + "infect:ci:cli": "@infect:ci:base --coverage=build/coverage-cli --min-msi=95 --configuration=infection-cli.json", "infect:ci": "@parallel infect:ci:unit infect:ci:db infect:ci:api", "infect:test": [ "@parallel test:unit:ci test:db:sqlite:ci test:api:ci", @@ -153,6 +156,10 @@ "@test:api:ci", "@infect:ci:api" ], + "infect:test:cli": [ + "@test:cli:ci", + "@infect:ci:cli" + ], "swagger:validate": "php-openapi validate docs/swagger/swagger.json", "swagger:inline": "php-openapi inline docs/swagger/swagger.json docs/swagger/swagger-inlined.json", "clean:dev": "rm -f data/database.sqlite && rm -f config/params/generated_config.php" diff --git a/infection-cli.json b/infection-cli.json new file mode 100644 index 00000000..60552d11 --- /dev/null +++ b/infection-cli.json @@ -0,0 +1,24 @@ +{ + "source": { + "directories": [ + "module/*/src" + ] + }, + "timeout": 5, + "logs": { + "text": "build/infection-cli/infection-log.txt", + "html": "build/infection-cli/infection-log.html", + "summary": "build/infection-cli/summary-log.txt", + "debug": "build/infection-cli/debug-log.txt" + }, + "tmpDir": "build/infection-cli/temp", + "phpUnit": { + "configDir": "." + }, + "testFrameworkOptions": "--configuration=phpunit-cli.xml", + "mutators": { + "@default": true, + "IdenticalEqual": false, + "NotIdenticalNotEqual": false + } +} From 95d84f354d2402e7e0dc6a57bcfec6591abcf20c Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Tue, 9 Aug 2022 19:48:43 +0200 Subject: [PATCH 005/108] Simplified tests config --- composer.json | 4 ++-- config/test/test_config.global.php | 18 +++++------------- 2 files changed, 7 insertions(+), 15 deletions(-) diff --git a/composer.json b/composer.json index 6a91dd85..36ee6ab5 100644 --- a/composer.json +++ b/composer.json @@ -107,7 +107,7 @@ ], "ci:parallel": [ "@parallel cs stan swagger:validate test:unit:ci test:db:sqlite:ci test:db:mysql test:db:maria test:db:postgres test:db:ms", - "@parallel infect:test:api infect:ci:unit infect:ci:db" + "@parallel infect:test:api infect:test:cli infect:ci:unit infect:ci:db" ], "cs": "phpcs", "cs:fix": "phpcbf", @@ -143,7 +143,7 @@ "infect:ci:db": "@infect:ci:base --coverage=build/coverage-db --min-msi=95 --configuration=infection-db.json", "infect:ci:api": "@infect:ci:base --coverage=build/coverage-api --min-msi=80 --configuration=infection-api.json", "infect:ci:cli": "@infect:ci:base --coverage=build/coverage-cli --min-msi=95 --configuration=infection-cli.json", - "infect:ci": "@parallel infect:ci:unit infect:ci:db infect:ci:api", + "infect:ci": "@parallel infect:ci:unit infect:ci:db infect:ci:api infect:ci:cli", "infect:test": [ "@parallel test:unit:ci test:db:sqlite:ci test:api:ci", "@infect:ci" diff --git a/config/test/test_config.global.php b/config/test/test_config.global.php index ddd7631d..75dd7457 100644 --- a/config/test/test_config.global.php +++ b/config/test/test_config.global.php @@ -9,7 +9,6 @@ use Laminas\ConfigAggregator\ConfigAggregator; use Laminas\Diactoros\Response\EmptyResponse; use Laminas\ServiceManager\Factory\InvokableFactory; use League\Event\EventDispatcher; -use Monolog\Handler\StreamHandler; use Monolog\Level; use PHPUnit\Runner\Version; use Psr\Container\ContainerInterface; @@ -22,11 +21,11 @@ use SebastianBergmann\CodeCoverage\Filter; use SebastianBergmann\CodeCoverage\Report\Html\Facade as Html; use SebastianBergmann\CodeCoverage\Report\PHP; use SebastianBergmann\CodeCoverage\Report\Xml\Facade as Xml; +use Shlinkio\Shlink\Common\Logger\LoggerType; use Symfony\Component\Console\Application; use Symfony\Component\Console\Event\ConsoleCommandEvent; use Symfony\Component\Console\Event\ConsoleTerminateEvent; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; -use Shlinkio\Shlink\Common\Logger\LoggerType; use function Laminas\Stratigility\middleware; use function Shlinkio\Shlink\Config\env; @@ -51,23 +50,16 @@ if ($isE2eTest && $generateCoverage) { /** * @param 'api'|'cli' $type - * @param array<'cov'|'xml'|'html'> $formats */ -$exportCoverage = static function (string $type = 'api', array $formats = ['cov', 'xml', 'html']) use (&$coverage): void { +$exportCoverage = static function (string $type = 'api') use (&$coverage): void { if ($coverage === null) { return; } $basePath = __DIR__ . '/../../build/coverage-' . $type; - - foreach ($formats as $format) { - match ($format) { - 'cov' => (new PHP())->process($coverage, $basePath . '.cov'), - 'xml' => (new Xml(Version::getVersionString()))->process($coverage, $basePath . '/coverage-xml'), - 'html' => (new Html())->process($coverage, $basePath . '/coverage-html'), - default => null, - }; - } + (new PHP())->process($coverage, $basePath . '.cov'); + (new Xml(Version::getVersionString()))->process($coverage, $basePath . '/coverage-xml'); + (new Html())->process($coverage, $basePath . '/coverage-html'); }; $buildDbConnection = static function (): array { From 474407dbc273ade5245bf4b91fe4fb33bda07b7c Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Wed, 10 Aug 2022 17:08:42 +0200 Subject: [PATCH 006/108] Ensured proper coverage is generated during CLI tests --- composer.json | 2 +- config/test/bootstrap_cli_tests.php | 8 ++++++++ config/test/test_config.global.php | 13 ++++++++++++- 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 36ee6ab5..627829aa 100644 --- a/composer.json +++ b/composer.json @@ -142,7 +142,7 @@ "infect:ci:unit": "@infect:ci:base --coverage=build/coverage-unit --min-msi=84", "infect:ci:db": "@infect:ci:base --coverage=build/coverage-db --min-msi=95 --configuration=infection-db.json", "infect:ci:api": "@infect:ci:base --coverage=build/coverage-api --min-msi=80 --configuration=infection-api.json", - "infect:ci:cli": "@infect:ci:base --coverage=build/coverage-cli --min-msi=95 --configuration=infection-cli.json", + "infect:ci:cli": "@infect:ci:base --coverage=build/coverage-cli --min-msi=80 --configuration=infection-cli.json", "infect:ci": "@parallel infect:ci:unit infect:ci:db infect:ci:api infect:ci:cli", "infect:test": [ "@parallel test:unit:ci test:db:sqlite:ci test:api:ci", diff --git a/config/test/bootstrap_cli_tests.php b/config/test/bootstrap_cli_tests.php index 703677d2..c3b9870c 100644 --- a/config/test/bootstrap_cli_tests.php +++ b/config/test/bootstrap_cli_tests.php @@ -6,6 +6,8 @@ namespace Shlinkio\Shlink\TestUtils; use Doctrine\ORM\EntityManager; use Psr\Container\ContainerInterface; +use function file_exists; +use function unlink; /** @var ContainerInterface $container */ $container = require __DIR__ . '/../container.php'; @@ -13,6 +15,12 @@ $testHelper = $container->get(Helper\TestHelper::class); $config = $container->get('config'); $em = $container->get(EntityManager::class); +// Delete old coverage in PHP, to avoid merging older executions with current one +$covFile = __DIR__ . '/../../build/coverage-cli.cov'; +if (file_exists($covFile)) { + unlink($covFile); +} + $testHelper->createTestDb(['bin/cli', 'db:create'], ['bin/cli', 'db:migrate']); CliTest\CliTestCase::setSeedFixturesCallback( static fn () => $testHelper->seedFixtures($em, $config['data_fixtures'] ?? []), diff --git a/config/test/test_config.global.php b/config/test/test_config.global.php index 75dd7457..e82d9da5 100644 --- a/config/test/test_config.global.php +++ b/config/test/test_config.global.php @@ -27,6 +27,7 @@ use Symfony\Component\Console\Event\ConsoleCommandEvent; use Symfony\Component\Console\Event\ConsoleTerminateEvent; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; +use function file_exists; use function Laminas\Stratigility\middleware; use function Shlinkio\Shlink\Config\env; use function sprintf; @@ -57,7 +58,15 @@ $exportCoverage = static function (string $type = 'api') use (&$coverage): void } $basePath = __DIR__ . '/../../build/coverage-' . $type; - (new PHP())->process($coverage, $basePath . '.cov'); + $covPath = $basePath . '.cov'; + + // Every CLI test runs on its own process and dumps the coverage afterwards. + // Try to load it and merge it, so that we end up with the whole coverage at the end. + if ($type === 'cli' && file_exists($covPath)) { + $coverage->merge(require $covPath); + } + + (new PHP())->process($coverage, $covPath); (new Xml(Version::getVersionString()))->process($coverage, $basePath . '/coverage-xml'); (new Html())->process($coverage, $basePath . '/coverage-html'); }; @@ -195,6 +204,7 @@ return [ $app = $callback(); $wrappedEventDispatcher = new EventDispatcher(); + // When the command starts, start collecting coverage $wrappedEventDispatcher->subscribeTo( ConsoleCommandEvent::class, static function () use (&$coverage): void { @@ -206,6 +216,7 @@ return [ $coverage?->start($id); }, ); + // When the command ends, stop collecting coverage $wrappedEventDispatcher->subscribeTo( ConsoleTerminateEvent::class, static function () use (&$coverage, $exportCoverage): void { From 10974902b57a193a794ac753fd2fe1ca32e92322 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Wed, 10 Aug 2022 17:09:54 +0200 Subject: [PATCH 007/108] Updated changelog --- CHANGELOG.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 71842087..93701f61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,23 @@ 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] +### Added +* *Nothing* + +### Changed +* [#1339](https://github.com/shlinkio/shlink/issues/1339) Added new test suite for CLI E2E tests. + +### Deprecated +* *Nothing* + +### Removed +* *Nothing* + +### Fixed +* *Nothing* + + ## [3.2.1] - 2022-08-08 ### Added * *Nothing* From 761b24e61490b77ab88351715d037fe2ef40bed3 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Wed, 10 Aug 2022 17:13:21 +0200 Subject: [PATCH 008/108] Added CLI tests to to CI pipeline --- .github/workflows/ci.yml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b1ce0d0b..61a06080 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,13 +34,16 @@ jobs: strategy: matrix: php-version: ['8.1'] - test-group: ['unit', 'api'] + test-group: ['unit', 'api', 'cli'] steps: - name: Checkout code uses: actions/checkout@v2 - - name: Start database server + - name: Start postgres database server if: ${{ matrix.test-group == 'api' }} run: docker-compose -f docker-compose.yml -f docker-compose.ci.yml up -d shlink_db_postgres + - name: Start maria database server + if: ${{ matrix.test-group == 'cli' }} + run: docker-compose -f docker-compose.yml -f docker-compose.ci.yml up -d shlink_db_maria - name: Use PHP uses: shivammathur/setup-php@v2 with: @@ -156,6 +159,7 @@ jobs: - run: mv build/coverage-unit/coverage-unit.cov build/coverage-unit.cov - run: mv build/coverage-db/coverage-db.cov build/coverage-db.cov - run: mv build/coverage-api/coverage-api.cov build/coverage-api.cov + - run: mv build/coverage-cli/coverage-cli.cov build/coverage-cli.cov - run: wget https://phar.phpunit.de/phpcov-8.2.1.phar - run: php phpcov-8.2.1.phar merge build --clover build/clover.xml - name: Publish coverage @@ -175,6 +179,7 @@ jobs: coverage-unit coverage-db coverage-api + coverage-cli build-docker-image: runs-on: ubuntu-22.04 From 71553988d5ff0109132e43bcd78584c8b8accf39 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Wed, 10 Aug 2022 17:21:55 +0200 Subject: [PATCH 009/108] Added cli mutation tests to pipeline, and referenced CLI tests in CONTRIBUTING file --- .github/workflows/ci.yml | 2 +- CONTRIBUTING.md | 5 ++++- config/test/bootstrap_cli_tests.php | 1 + 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 61a06080..c37524ab 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -112,7 +112,7 @@ jobs: strategy: matrix: php-version: ['8.1'] - test-group: ['unit', 'db', 'api'] + test-group: ['unit', 'db', 'api', 'cli'] steps: - name: Checkout code uses: actions/checkout@v2 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2024adca..5f620893 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -102,7 +102,9 @@ In order to ensure stability and no regressions are introduced while developing Since the app instance is run on a process different from the one running the tests, when a test fails it might not be obvious why. To help debugging that, the app will dump all its logs inside `data/log/api-tests`, where you will find the `shlink.log` and `access.log` files. -* **CLI tests**: *TBD. Once included, its purpose will be the same as API tests, but running through the command line* +* **CLI tests**: These are E2E tests too, but they test console commands instead of REST endpoints. + + They use Maria DB as the database engine, and include the same fixtures as the API tests, that ensure the same data exists at the beginning of the execution. Depending on the kind of contribution, maybe not all kinds of tests are needed, but the more you provide, the better. @@ -119,6 +121,7 @@ Depending on the kind of contribution, maybe not all kinds of tests are needed, For example, `test:db:postgres`. * Run `./indocker composer test:api` to run API E2E tests. For these, the Postgres database engine is used. +* Run `./indocker composer test:cli` to run CLI E2E tests. For these, the Maria DB database engine is used. * Run `./indocker composer infect:test` to run both unit and database tests (over sqlite) and then apply mutations to them with [infection](https://infection.github.io/). * Run `./indocker composer ci` to run all previous commands together. This command is run during the project's continuous integration. * Run `./indocker composer ci:parallel` to do the same as in previous case, but parallelizing non-conflicting tasks as much as possible. diff --git a/config/test/bootstrap_cli_tests.php b/config/test/bootstrap_cli_tests.php index c3b9870c..893bdfd7 100644 --- a/config/test/bootstrap_cli_tests.php +++ b/config/test/bootstrap_cli_tests.php @@ -6,6 +6,7 @@ namespace Shlinkio\Shlink\TestUtils; use Doctrine\ORM\EntityManager; use Psr\Container\ContainerInterface; + use function file_exists; use function unlink; From e3b6c061c457922196e87642d7c5431b0b75c91d Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Fri, 12 Aug 2022 08:35:10 +0200 Subject: [PATCH 010/108] Extracted definition of unit tests job to local reusable workflow --- .github/workflows/ci.yml | 55 +++++++++++++--------------------- .github/workflows/ci/tests.yml | 43 ++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 35 deletions(-) create mode 100644 .github/workflows/ci/tests.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c37524ab..f7d37f89 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,39 +29,20 @@ jobs: run: composer install --no-interaction --prefer-dist - run: composer ${{ matrix.command }} - tests: - runs-on: ubuntu-22.04 - strategy: - matrix: - php-version: ['8.1'] - test-group: ['unit', 'api', 'cli'] - steps: - - name: Checkout code - uses: actions/checkout@v2 - - name: Start postgres database server - if: ${{ matrix.test-group == 'api' }} - run: docker-compose -f docker-compose.yml -f docker-compose.ci.yml up -d shlink_db_postgres - - name: Start maria database server - if: ${{ matrix.test-group == 'cli' }} - run: docker-compose -f docker-compose.yml -f docker-compose.ci.yml up -d shlink_db_maria - - name: Use PHP - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php-version }} - tools: composer - extensions: openswoole-4.11.1 - coverage: pcov - ini-values: pcov.directory=module - - name: Install dependencies - run: composer install --no-interaction --prefer-dist - - run: composer test:${{ matrix.test-group }}:ci - - uses: actions/upload-artifact@v2 - if: ${{ matrix.php-version == '8.1' }} - with: - name: coverage-${{ matrix.test-group }} - path: | - build/coverage-${{ matrix.test-group }} - build/coverage-${{ matrix.test-group }}.cov + unit-tests: + uses: './.github/workflows/ci/tests.yml' + with: + test-group: unit + + api-tests: + uses: './.github/workflows/ci/tests.yml' + with: + test-group: api + + cli-tests: + uses: './.github/workflows/ci/tests.yml' + with: + test-group: cli db-tests: runs-on: ubuntu-22.04 @@ -106,7 +87,9 @@ jobs: mutation-tests: needs: - - tests + - unit-tests + - api-tests + - cli-tests - db-tests runs-on: ubuntu-22.04 strategy: @@ -138,7 +121,9 @@ jobs: upload-coverage: needs: - - tests + - unit-tests + - api-tests + - cli-tests - db-tests runs-on: ubuntu-22.04 strategy: diff --git a/.github/workflows/ci/tests.yml b/.github/workflows/ci/tests.yml new file mode 100644 index 00000000..1d2f5063 --- /dev/null +++ b/.github/workflows/ci/tests.yml @@ -0,0 +1,43 @@ +name: Tests + +on: + workflow_call: + inputs: + test-group: + type: string + required: true + description: One of unit, api or cli + +jobs: + tests: + runs-on: ubuntu-22.04 + strategy: + matrix: + php-version: ['8.1'] + steps: + - name: Checkout code + uses: actions/checkout@v2 + - name: Start postgres database server + if: ${{ inputs.test-group == 'api' }} + run: docker-compose -f docker-compose.yml -f docker-compose.ci.yml up -d shlink_db_postgres + - name: Start maria database server + if: ${{ inputs.test-group == 'cli' }} + run: docker-compose -f docker-compose.yml -f docker-compose.ci.yml up -d shlink_db_maria + - name: Use PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-version }} + tools: composer + extensions: openswoole-4.11.1 + coverage: pcov + ini-values: pcov.directory=module + - name: Install dependencies + run: composer install --no-interaction --prefer-dist + - run: composer test:${{ inputs.test-group }}:ci + - uses: actions/upload-artifact@v2 + if: ${{ matrix.php-version == '8.1' }} + with: + name: coverage-${{ inputs.test-group }} + path: | + build/coverage-${{ inputs.test-group }} + build/coverage-${{ inputs.test-group }}.cov From 51536f8746178ffebc686db7ebc0dd891b370e70 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Fri, 12 Aug 2022 09:13:04 +0200 Subject: [PATCH 011/108] Moved reusable ci tests workflow to workflows folder --- .github/workflows/{ci/tests.yml => ci-tests.yml} | 0 .github/workflows/ci.yml | 6 +++--- 2 files changed, 3 insertions(+), 3 deletions(-) rename .github/workflows/{ci/tests.yml => ci-tests.yml} (100%) diff --git a/.github/workflows/ci/tests.yml b/.github/workflows/ci-tests.yml similarity index 100% rename from .github/workflows/ci/tests.yml rename to .github/workflows/ci-tests.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f7d37f89..6117d856 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,17 +30,17 @@ jobs: - run: composer ${{ matrix.command }} unit-tests: - uses: './.github/workflows/ci/tests.yml' + uses: './.github/workflows/ci-tests.yml' with: test-group: unit api-tests: - uses: './.github/workflows/ci/tests.yml' + uses: './.github/workflows/ci-tests.yml' with: test-group: api cli-tests: - uses: './.github/workflows/ci/tests.yml' + uses: './.github/workflows/ci-tests.yml' with: test-group: cli From 63832306789d4021e7ab5b16046bedc9613bdc3a Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Fri, 12 Aug 2022 09:30:52 +0200 Subject: [PATCH 012/108] Extracted DB tests and mutation tests to reusable workflows --- .github/workflows/ci-db-tests.yml | 50 ++++++++++ .github/workflows/ci-mutation-tests.yml | 38 ++++++++ .github/workflows/ci.yml | 123 ++++++++++-------------- 3 files changed, 140 insertions(+), 71 deletions(-) create mode 100644 .github/workflows/ci-db-tests.yml create mode 100644 .github/workflows/ci-mutation-tests.yml diff --git a/.github/workflows/ci-db-tests.yml b/.github/workflows/ci-db-tests.yml new file mode 100644 index 00000000..545e0e53 --- /dev/null +++ b/.github/workflows/ci-db-tests.yml @@ -0,0 +1,50 @@ +name: Database tests + +on: + workflow_call: + inputs: + platform: + type: string + required: true + description: One of sqlite:ci, mysql, maria, postgres or ms + +jobs: + db-tests: + runs-on: ubuntu-22.04 + strategy: + matrix: + php-version: [ '8.1' ] + env: + LC_ALL: C + steps: + - name: Checkout code + uses: actions/checkout@v2 + - name: Install MSSQL ODBC + if: ${{ inputs.platform == 'ms' }} + run: sudo ./data/infra/ci/install-ms-odbc.sh + - name: Start database server + if: ${{ inputs.platform != 'sqlite:ci' }} + run: docker-compose -f docker-compose.yml -f docker-compose.ci.yml up -d shlink_db_${{ inputs.platform }} + - name: Use PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-version }} + tools: composer + extensions: openswoole-4.11.1, pdo_sqlsrv-5.10.1 + coverage: pcov + ini-values: pcov.directory=module + - name: Install dependencies + run: composer install --no-interaction --prefer-dist + - name: Create test database + if: ${{ inputs.platform == 'ms' }} + run: docker-compose exec -T shlink_db_ms /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P 'Passw0rd!' -Q "CREATE DATABASE shlink_test;" + - name: Run tests + run: composer test:db:${{ inputs.platform }} + - name: Upload code coverage + uses: actions/upload-artifact@v2 + if: ${{ matrix.php-version == '8.1' && inputs.platform == 'sqlite:ci' }} + with: + name: coverage-db + path: | + build/coverage-db + build/coverage-db.cov diff --git a/.github/workflows/ci-mutation-tests.yml b/.github/workflows/ci-mutation-tests.yml new file mode 100644 index 00000000..4ed47af4 --- /dev/null +++ b/.github/workflows/ci-mutation-tests.yml @@ -0,0 +1,38 @@ +name: Mutation tests + +on: + workflow_call: + inputs: + test-group: + type: string + required: true + description: One of unit, db, api or cli + +jobs: + mutation-tests: + runs-on: ubuntu-22.04 + strategy: + matrix: + php-version: [ '8.1' ] + steps: + - name: Checkout code + uses: actions/checkout@v2 + - name: Use PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-version }} + tools: composer + extensions: openswoole-4.11.1 + coverage: pcov + ini-values: pcov.directory=module + - name: Install dependencies + run: composer install --no-interaction --prefer-dist + - uses: actions/download-artifact@v2 + with: + path: build + - if: ${{ inputs.test-group == 'unit' }} + run: composer infect:ci:unit + env: + INFECTION_BADGE_API_KEY: ${{ secrets.INFECTION_BADGE_API_KEY }} + - if: ${{ inputs.test-group != 'unit' }} + run: composer infect:ci:${{ inputs.test-group }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6117d856..65fb6254 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,87 +44,65 @@ jobs: with: test-group: cli - db-tests: - runs-on: ubuntu-22.04 - strategy: - matrix: - php-version: ['8.1'] - platform: ['sqlite:ci', 'mysql', 'maria', 'postgres', 'ms'] - env: - LC_ALL: C - steps: - - name: Checkout code - uses: actions/checkout@v2 - - name: Install MSSQL ODBC - if: ${{ matrix.platform == 'ms' }} - run: sudo ./data/infra/ci/install-ms-odbc.sh - - name: Start database server - if: ${{ matrix.platform != 'sqlite:ci' }} - run: docker-compose -f docker-compose.yml -f docker-compose.ci.yml up -d shlink_db_${{ matrix.platform }} - - name: Use PHP - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php-version }} - tools: composer - extensions: openswoole-4.11.1, pdo_sqlsrv-5.10.1 - coverage: pcov - ini-values: pcov.directory=module - - name: Install dependencies - run: composer install --no-interaction --prefer-dist - - name: Create test database - if: ${{ matrix.platform == 'ms' }} - run: docker-compose exec -T shlink_db_ms /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P 'Passw0rd!' -Q "CREATE DATABASE shlink_test;" - - name: Run tests - run: composer test:db:${{ matrix.platform }} - - name: Upload code coverage - uses: actions/upload-artifact@v2 - if: ${{ matrix.php-version == '8.1' && matrix.platform == 'sqlite:ci' }} - with: - name: coverage-db - path: | - build/coverage-db - build/coverage-db.cov + sqlite-db-tests: + uses: './.github/workflows/ci-db-tests.yml' + with: + platform: 'sqlite:ci' - mutation-tests: + mysql-db-tests: + uses: './.github/workflows/ci-db-tests.yml' + with: + platform: 'mysql' + + maria-db-tests: + uses: './.github/workflows/ci-db-tests.yml' + with: + platform: 'maria' + + postgres-db-tests: + uses: './.github/workflows/ci-db-tests.yml' + with: + platform: 'postgres' + + ms-db-tests: + uses: './.github/workflows/ci-db-tests.yml' + with: + platform: 'ms' + + unit-mutation-tests: needs: - unit-tests + uses: './github/workflows/ci-mutation-tests.yml' + with: + test-group: unit + + db-mutation-tests: + needs: + - sqlite-db-tests + uses: './github/workflows/ci-mutation-tests.yml' + with: + test-group: db + + api-mutation-tests: + needs: - api-tests + uses: './github/workflows/ci-mutation-tests.yml' + with: + test-group: api + + cli-mutation-tests: + needs: - cli-tests - - db-tests - runs-on: ubuntu-22.04 - strategy: - matrix: - php-version: ['8.1'] - test-group: ['unit', 'db', 'api', 'cli'] - steps: - - name: Checkout code - uses: actions/checkout@v2 - - name: Use PHP - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php-version }} - tools: composer - extensions: openswoole-4.11.1 - coverage: pcov - ini-values: pcov.directory=module - - name: Install dependencies - run: composer install --no-interaction --prefer-dist - - uses: actions/download-artifact@v2 - with: - path: build - - if: ${{ matrix.test-group == 'unit' }} - run: composer infect:ci:unit - env: - INFECTION_BADGE_API_KEY: ${{ secrets.INFECTION_BADGE_API_KEY }} - - if: ${{ matrix.test-group != 'unit' }} - run: composer infect:ci:${{ matrix.test-group }} + uses: './github/workflows/ci-mutation-tests.yml' + with: + test-group: cli upload-coverage: needs: - unit-tests - api-tests - cli-tests - - db-tests + - sqlite-db-tests runs-on: ubuntu-22.04 strategy: matrix: @@ -154,7 +132,10 @@ jobs: delete-artifacts: needs: - - mutation-tests + - unit-mutation-tests + - db-mutation-tests + - api-mutation-tests + - cli-mutation-tests - upload-coverage runs-on: ubuntu-22.04 steps: From 0f796859f224526539dc1d2a235be0a64f679b89 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Fri, 12 Aug 2022 09:32:30 +0200 Subject: [PATCH 013/108] Fixed typo in ci workflow --- .github/workflows/ci.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 65fb6254..f3ed416f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -72,28 +72,28 @@ jobs: unit-mutation-tests: needs: - unit-tests - uses: './github/workflows/ci-mutation-tests.yml' + uses: './.github/workflows/ci-mutation-tests.yml' with: test-group: unit db-mutation-tests: needs: - sqlite-db-tests - uses: './github/workflows/ci-mutation-tests.yml' + uses: './.github/workflows/ci-mutation-tests.yml' with: test-group: db api-mutation-tests: needs: - api-tests - uses: './github/workflows/ci-mutation-tests.yml' + uses: './.github/workflows/ci-mutation-tests.yml' with: test-group: api cli-mutation-tests: needs: - cli-tests - uses: './github/workflows/ci-mutation-tests.yml' + uses: './.github/workflows/ci-mutation-tests.yml' with: test-group: cli From 23f92179ad6f366002c0173d58b7d5e90ba838c0 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Fri, 12 Aug 2022 18:10:45 +0200 Subject: [PATCH 014/108] Optimized how and when code coverage reports are generated for different types of tests --- CONTRIBUTING.md | 5 ++-- composer.json | 37 +++++++++++------------------- config/test/test_config.global.php | 15 ++++++++---- 3 files changed, 25 insertions(+), 32 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5f620893..feb437ad 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -123,8 +123,7 @@ Depending on the kind of contribution, maybe not all kinds of tests are needed, * Run `./indocker composer test:api` to run API E2E tests. For these, the Postgres database engine is used. * Run `./indocker composer test:cli` to run CLI E2E tests. For these, the Maria DB database engine is used. * Run `./indocker composer infect:test` to run both unit and database tests (over sqlite) and then apply mutations to them with [infection](https://infection.github.io/). -* Run `./indocker composer ci` to run all previous commands together. This command is run during the project's continuous integration. -* Run `./indocker composer ci:parallel` to do the same as in previous case, but parallelizing non-conflicting tasks as much as possible. +* Run `./indocker composer ci` to run all previous commands together, parallelizing non-conflicting tasks as much as possible. ## Pull request process @@ -136,7 +135,7 @@ Once everything is clear, to provide a pull request to this project, you should The base branch should always be `develop`, and the target branch for the pull request should also be `develop`. -Before your branch can be merged, all the checks described in [Running code checks](#running-code-checks) have to be passing. You can verify that manually by running `./indocker composer ci:parallel`, or wait for the build to be run automatically after the pull request is created. +Before your branch can be merged, all the checks described in [Running code checks](#running-code-checks) have to be passing. You can verify that manually by running `./indocker composer ci`, or wait for the build to be run automatically after the pull request is created. ## Architectural Decision Records diff --git a/composer.json b/composer.json index 627829aa..0cc0e65c 100644 --- a/composer.json +++ b/composer.json @@ -99,13 +99,6 @@ }, "scripts": { "ci": [ - "@cs", - "@stan", - "@swagger:validate", - "@test:ci", - "@infect:ci" - ], - "ci:parallel": [ "@parallel cs stan swagger:validate test:unit:ci test:db:sqlite:ci test:db:mysql test:db:maria test:db:postgres test:db:ms", "@parallel infect:test:api infect:test:cli infect:ci:unit infect:ci:db" ], @@ -113,19 +106,11 @@ "cs:fix": "phpcbf", "stan": "APP_ENV=test php vendor/bin/phpstan analyse module/*/src module/*/config config docker/config data/migrations --level=8", "test": [ - "@test:unit", - "@test:db", - "@test:api", - "@test:cli" + "@parallel test:unit test:db", + "@parallel test:api test:cli" ], - "test:ci": [ - "@test:unit:ci", - "@test:db", - "@test:api:ci", - "@test:cli:ci" - ], - "test:unit": "@php vendor/bin/phpunit --order-by=random --colors=always --coverage-php build/coverage-unit.cov --testdox", - "test:unit:ci": "@test:unit --coverage-xml=build/coverage-unit/coverage-xml --log-junit=build/coverage-unit/junit.xml", + "test:unit": "@php vendor/bin/phpunit --order-by=random --colors=always --testdox", + "test:unit:ci": "@test:unit --coverage-php=build/coverage-unit.cov --coverage-xml=build/coverage-unit/coverage-xml --log-junit=build/coverage-unit/junit.xml", "test:unit:pretty": "@test:unit --coverage-html build/coverage-unit/coverage-html", "test:db": "@parallel test:db:sqlite:ci test:db:mysql test:db:maria test:db:postgres test:db:ms", "test:db:sqlite": "APP_ENV=test php vendor/bin/phpunit --order-by=random --colors=always --testdox -c phpunit-db.xml", @@ -136,8 +121,10 @@ "test:db:ms": "DB_DRIVER=mssql composer test:db:sqlite", "test:api": "bin/test/run-api-tests.sh", "test:api:ci": "GENERATE_COVERAGE=yes composer test:api", - "test:cli": "APP_ENV=test DB_DRIVER=maria TEST_ENV=cli php vendor/bin/phpunit --order-by=random --colors=always --testdox -c phpunit-cli.xml", - "test:cli:ci": "GENERATE_COVERAGE=yes composer test:cli -- --log-junit=build/coverage-cli/junit.xml", + "test:api:pretty": "GENERATE_COVERAGE=pretty composer test:api", + "test:cli": "APP_ENV=test DB_DRIVER=maria TEST_ENV=cli php vendor/bin/phpunit --order-by=random --colors=always --testdox -c phpunit-cli.xml --log-junit=build/coverage-cli/junit.xml", + "test:cli:ci": "GENERATE_COVERAGE=yes composer test:cli", + "test:cli:pretty": "GENERATE_COVERAGE=pretty composer test:cli", "infect:ci:base": "infection --threads=4 --log-verbosity=default --only-covered --only-covering-test-cases --skip-initial-tests", "infect:ci:unit": "@infect:ci:base --coverage=build/coverage-unit --min-msi=84", "infect:ci:db": "@infect:ci:base --coverage=build/coverage-db --min-msi=95 --configuration=infection-db.json", @@ -166,12 +153,10 @@ }, "scripts-descriptions": { "ci": "Alias for \"cs\", \"stan\", \"swagger:validate\", \"test:ci\" and \"infect:ci\"", - "ci:parallel": "Same as \"ci\", but parallelizing tasks as much as possible", "cs": "Checks coding styles", "cs:fix": "Fixes coding styles, when possible", "stan": "Inspects code with phpstan", "test": "Runs all test suites", - "test:ci": "Runs all test suites, generating all needed reports and logs for CI envs", "test:unit": "Runs unit test suites", "test:unit:ci": "Runs unit test suites, generating all needed reports and logs for CI envs", "test:unit:pretty": "Runs unit test suites and generates an HTML code coverage report", @@ -183,7 +168,11 @@ "test:db:postgres": "Runs database test suites on a PostgreSQL database", "test:db:ms": "Runs database test suites on a Microsoft SQL Server database", "test:api": "Runs API test suites", - "test:api:ci": "Runs API test suites, and generates code coverage reports", + "test:api:ci": "Runs API test suites, and generates code coverage for CI", + "test:api:pretty": "Runs API test suites, and generates code coverage in HTML format", + "test:cli": "Runs CLI test suites", + "test:cli:ci": "Runs CLI test suites, and generates code coverage for CI", + "test:cli:pretty": "Runs CLI test suites, and generates code coverage in HTML format", "infect:ci": "Checks unit and db tests quality applying mutation testing with existing reports and logs", "infect:ci:unit": "Checks unit tests quality applying mutation testing with existing reports and logs", "infect:ci:db": "Checks db tests quality applying mutation testing with existing reports and logs", diff --git a/config/test/test_config.global.php b/config/test/test_config.global.php index e82d9da5..8998dd22 100644 --- a/config/test/test_config.global.php +++ b/config/test/test_config.global.php @@ -28,6 +28,7 @@ use Symfony\Component\Console\Event\ConsoleTerminateEvent; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; use function file_exists; +use function Functional\contains; use function Laminas\Stratigility\middleware; use function Shlinkio\Shlink\Config\env; use function sprintf; @@ -39,7 +40,8 @@ use const ShlinkioTest\Shlink\SWOOLE_TESTING_PORT; $isApiTest = env('TEST_ENV') === 'api'; $isCliTest = env('TEST_ENV') === 'cli'; $isE2eTest = $isApiTest || $isCliTest; -$generateCoverage = env('GENERATE_COVERAGE') === 'yes'; +$coverageType = env('GENERATE_COVERAGE'); +$generateCoverage = contains(['yes', 'pretty'], $coverageType); $coverage = null; if ($isE2eTest && $generateCoverage) { @@ -52,7 +54,7 @@ if ($isE2eTest && $generateCoverage) { /** * @param 'api'|'cli' $type */ -$exportCoverage = static function (string $type = 'api') use (&$coverage): void { +$exportCoverage = static function (string $type = 'api') use (&$coverage, $coverageType): void { if ($coverage === null) { return; } @@ -66,9 +68,12 @@ $exportCoverage = static function (string $type = 'api') use (&$coverage): void $coverage->merge(require $covPath); } - (new PHP())->process($coverage, $covPath); - (new Xml(Version::getVersionString()))->process($coverage, $basePath . '/coverage-xml'); - (new Html())->process($coverage, $basePath . '/coverage-html'); + if ($coverageType === 'pretty') { + (new Html())->process($coverage, $basePath . '/coverage-html'); + } else { + (new PHP())->process($coverage, $covPath); + (new Xml(Version::getVersionString()))->process($coverage, $basePath . '/coverage-xml'); + } }; $buildDbConnection = static function (): array { From 6654f45cb8fe24a032b4503d66f34fc02657d2e6 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Fri, 12 Aug 2022 18:19:12 +0200 Subject: [PATCH 015/108] Updated upload/download artifact actions --- .github/workflows/ci-db-tests.yml | 2 +- .github/workflows/ci-mutation-tests.yml | 2 +- .github/workflows/ci-tests.yml | 2 +- .github/workflows/ci.yml | 2 +- .github/workflows/publish-release.yml | 4 ++-- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci-db-tests.yml b/.github/workflows/ci-db-tests.yml index 545e0e53..6274ef5c 100644 --- a/.github/workflows/ci-db-tests.yml +++ b/.github/workflows/ci-db-tests.yml @@ -41,7 +41,7 @@ jobs: - name: Run tests run: composer test:db:${{ inputs.platform }} - name: Upload code coverage - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 if: ${{ matrix.php-version == '8.1' && inputs.platform == 'sqlite:ci' }} with: name: coverage-db diff --git a/.github/workflows/ci-mutation-tests.yml b/.github/workflows/ci-mutation-tests.yml index 4ed47af4..297eb825 100644 --- a/.github/workflows/ci-mutation-tests.yml +++ b/.github/workflows/ci-mutation-tests.yml @@ -27,7 +27,7 @@ jobs: ini-values: pcov.directory=module - name: Install dependencies run: composer install --no-interaction --prefer-dist - - uses: actions/download-artifact@v2 + - uses: actions/download-artifact@v3 with: path: build - if: ${{ inputs.test-group == 'unit' }} diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml index 1d2f5063..97b0622b 100644 --- a/.github/workflows/ci-tests.yml +++ b/.github/workflows/ci-tests.yml @@ -34,7 +34,7 @@ jobs: - name: Install dependencies run: composer install --no-interaction --prefer-dist - run: composer test:${{ inputs.test-group }}:ci - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v3 if: ${{ matrix.php-version == '8.1' }} with: name: coverage-${{ inputs.test-group }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f3ed416f..b71f3bab 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -116,7 +116,7 @@ jobs: php-version: ${{ matrix.php-version }} coverage: pcov ini-values: pcov.directory=module - - uses: actions/download-artifact@v2 + - uses: actions/download-artifact@v3 with: path: build - run: mv build/coverage-unit/coverage-unit.cov build/coverage-unit.cov diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index 4903fe52..6ff6ec15 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -25,7 +25,7 @@ jobs: run: ./build.sh ${GITHUB_REF#refs/tags/v} - if: ${{ matrix.swoole == 'no' }} run: ./build.sh ${GITHUB_REF#refs/tags/v} --no-swoole - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v3 with: name: dist-files-${{ matrix.php-version }}-${{ matrix.swoole }} path: build @@ -36,7 +36,7 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v2 - - uses: actions/download-artifact@v2 + - uses: actions/download-artifact@v3 with: path: build - name: Publish release with assets From d7d0e11f2c1d6074306f7df30746f081ca9d9e55 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Fri, 12 Aug 2022 20:32:16 +0200 Subject: [PATCH 016/108] Added cache for PHP extensions in CI pipeline --- .github/workflows/ci-db-tests.yml | 16 +++++++++++++++- .github/workflows/ci-mutation-tests.yml | 17 ++++++++++++++++- .github/workflows/ci-tests.yml | 17 ++++++++++++++++- .github/workflows/ci.yml | 17 ++++++++++++++++- 4 files changed, 63 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci-db-tests.yml b/.github/workflows/ci-db-tests.yml index 6274ef5c..4f54e2ea 100644 --- a/.github/workflows/ci-db-tests.yml +++ b/.github/workflows/ci-db-tests.yml @@ -16,6 +16,7 @@ jobs: php-version: [ '8.1' ] env: LC_ALL: C + extensions: openswoole-4.11.1, pdo_sqlsrv-5.10.1 steps: - name: Checkout code uses: actions/checkout@v2 @@ -25,12 +26,25 @@ jobs: - name: Start database server if: ${{ inputs.platform != 'sqlite:ci' }} run: docker-compose -f docker-compose.yml -f docker-compose.ci.yml up -d shlink_db_${{ inputs.platform }} + - name: Setup cache environment + id: extcache + uses: shivammathur/cache-extensions@v1 + with: + php-version: ${{ matrix.php-version }} + extensions: ${{ env.extensions }} + key: db-tests-extensions + - name: Cache extensions + uses: actions/cache@v2 + with: + path: ${{ steps.extcache.outputs.dir }} + key: ${{ steps.extcache.outputs.key }} + restore-keys: ${{ steps.extcache.outputs.key }} - name: Use PHP uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php-version }} tools: composer - extensions: openswoole-4.11.1, pdo_sqlsrv-5.10.1 + extensions: ${{ env.extensions }} coverage: pcov ini-values: pcov.directory=module - name: Install dependencies diff --git a/.github/workflows/ci-mutation-tests.yml b/.github/workflows/ci-mutation-tests.yml index 297eb825..945f0abd 100644 --- a/.github/workflows/ci-mutation-tests.yml +++ b/.github/workflows/ci-mutation-tests.yml @@ -14,15 +14,30 @@ jobs: strategy: matrix: php-version: [ '8.1' ] + env: + extensions: openswoole-4.11.1 steps: - name: Checkout code uses: actions/checkout@v2 + - name: Setup cache environment + id: extcache + uses: shivammathur/cache-extensions@v1 + with: + php-version: ${{ matrix.php-version }} + extensions: ${{ env.extensions }} + key: mutation-tests-extensions + - name: Cache extensions + uses: actions/cache@v2 + with: + path: ${{ steps.extcache.outputs.dir }} + key: ${{ steps.extcache.outputs.key }} + restore-keys: ${{ steps.extcache.outputs.key }} - name: Use PHP uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php-version }} tools: composer - extensions: openswoole-4.11.1 + extensions: ${{ env.extensions }} coverage: pcov ini-values: pcov.directory=module - name: Install dependencies diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml index 97b0622b..a0ed17ea 100644 --- a/.github/workflows/ci-tests.yml +++ b/.github/workflows/ci-tests.yml @@ -14,6 +14,8 @@ jobs: strategy: matrix: php-version: ['8.1'] + env: + extensions: openswoole-4.11.1 steps: - name: Checkout code uses: actions/checkout@v2 @@ -23,12 +25,25 @@ jobs: - name: Start maria database server if: ${{ inputs.test-group == 'cli' }} run: docker-compose -f docker-compose.yml -f docker-compose.ci.yml up -d shlink_db_maria + - name: Setup cache environment + id: extcache + uses: shivammathur/cache-extensions@v1 + with: + php-version: ${{ matrix.php-version }} + extensions: ${{ env.extensions }} + key: tests-extensions + - name: Cache extensions + uses: actions/cache@v2 + with: + path: ${{ steps.extcache.outputs.dir }} + key: ${{ steps.extcache.outputs.key }} + restore-keys: ${{ steps.extcache.outputs.key }} - name: Use PHP uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php-version }} tools: composer - extensions: openswoole-4.11.1 + extensions: ${{ env.extensions }} coverage: pcov ini-values: pcov.directory=module - name: Install dependencies diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b71f3bab..397bb9ab 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,15 +15,30 @@ jobs: matrix: php-version: ['8.1'] command: ['cs', 'stan', 'swagger:validate'] + env: + extensions: openswoole-4.11.1 steps: - name: Checkout code uses: actions/checkout@v2 + - name: Setup cache environment + id: extcache + uses: shivammathur/cache-extensions@v1 + with: + php-version: ${{ matrix.php-version }} + extensions: ${{ env.extensions }} + key: tests-extensions + - name: Cache extensions + uses: actions/cache@v2 + with: + path: ${{ steps.extcache.outputs.dir }} + key: ${{ steps.extcache.outputs.key }} + restore-keys: ${{ steps.extcache.outputs.key }} - name: Use PHP uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php-version }} tools: composer - extensions: openswoole-4.11.1 + extensions: ${{ env.extensions }} coverage: none - name: Install dependencies run: composer install --no-interaction --prefer-dist From 14c2ff55453a1b4e8229199ffe313879533f698f Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Fri, 12 Aug 2022 20:35:59 +0200 Subject: [PATCH 017/108] Ensured unique cache key --- .github/workflows/ci-db-tests.yml | 2 +- .github/workflows/ci-mutation-tests.yml | 2 +- .github/workflows/ci-tests.yml | 2 +- .github/workflows/ci.yml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci-db-tests.yml b/.github/workflows/ci-db-tests.yml index 4f54e2ea..7a4aa3d1 100644 --- a/.github/workflows/ci-db-tests.yml +++ b/.github/workflows/ci-db-tests.yml @@ -32,7 +32,7 @@ jobs: with: php-version: ${{ matrix.php-version }} extensions: ${{ env.extensions }} - key: db-tests-extensions + key: db-tests-extensions-${{ inputs.platform }} - name: Cache extensions uses: actions/cache@v2 with: diff --git a/.github/workflows/ci-mutation-tests.yml b/.github/workflows/ci-mutation-tests.yml index 945f0abd..16937a2e 100644 --- a/.github/workflows/ci-mutation-tests.yml +++ b/.github/workflows/ci-mutation-tests.yml @@ -25,7 +25,7 @@ jobs: with: php-version: ${{ matrix.php-version }} extensions: ${{ env.extensions }} - key: mutation-tests-extensions + key: mutation-tests-extensions-${{ inputs.test-group }} - name: Cache extensions uses: actions/cache@v2 with: diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml index a0ed17ea..078bc551 100644 --- a/.github/workflows/ci-tests.yml +++ b/.github/workflows/ci-tests.yml @@ -31,7 +31,7 @@ jobs: with: php-version: ${{ matrix.php-version }} extensions: ${{ env.extensions }} - key: tests-extensions + key: tests-extensions-${{ inputs.test-group }} - name: Cache extensions uses: actions/cache@v2 with: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 397bb9ab..c76f47bc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,7 +26,7 @@ jobs: with: php-version: ${{ matrix.php-version }} extensions: ${{ env.extensions }} - key: tests-extensions + key: tests-extensions-${{ matrix.command }} - name: Cache extensions uses: actions/cache@v2 with: From 1613975e0e5fb3d3157a8d0867fb6e07caa6bd89 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 13 Aug 2022 10:11:41 +0200 Subject: [PATCH 018/108] Improved cache keys for extensions in CI workflow to support several PHP versions when needed --- .github/workflows/ci-db-tests.yml | 2 +- .github/workflows/ci-mutation-tests.yml | 2 +- .github/workflows/ci-tests.yml | 2 +- .github/workflows/ci.yml | 2 +- .github/workflows/publish-release.yml | 17 ++++++++++++++++- .github/workflows/publish-swagger-spec.yml | 17 ++++++++++++++++- 6 files changed, 36 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci-db-tests.yml b/.github/workflows/ci-db-tests.yml index 7a4aa3d1..234167d1 100644 --- a/.github/workflows/ci-db-tests.yml +++ b/.github/workflows/ci-db-tests.yml @@ -32,7 +32,7 @@ jobs: with: php-version: ${{ matrix.php-version }} extensions: ${{ env.extensions }} - key: db-tests-extensions-${{ inputs.platform }} + key: db-tests-extensions-${{ matrix.php-version }}-${{ inputs.platform }} - name: Cache extensions uses: actions/cache@v2 with: diff --git a/.github/workflows/ci-mutation-tests.yml b/.github/workflows/ci-mutation-tests.yml index 16937a2e..85ff6a8e 100644 --- a/.github/workflows/ci-mutation-tests.yml +++ b/.github/workflows/ci-mutation-tests.yml @@ -25,7 +25,7 @@ jobs: with: php-version: ${{ matrix.php-version }} extensions: ${{ env.extensions }} - key: mutation-tests-extensions-${{ inputs.test-group }} + key: mutation-tests-extensions-${{ matrix.php-version }}-${{ inputs.test-group }} - name: Cache extensions uses: actions/cache@v2 with: diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml index 078bc551..630c9614 100644 --- a/.github/workflows/ci-tests.yml +++ b/.github/workflows/ci-tests.yml @@ -31,7 +31,7 @@ jobs: with: php-version: ${{ matrix.php-version }} extensions: ${{ env.extensions }} - key: tests-extensions-${{ inputs.test-group }} + key: tests-extensions-${{ matrix.php-version }}-${{ inputs.test-group }} - name: Cache extensions uses: actions/cache@v2 with: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c76f47bc..2be038e2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,7 +26,7 @@ jobs: with: php-version: ${{ matrix.php-version }} extensions: ${{ env.extensions }} - key: tests-extensions-${{ matrix.command }} + key: tests-extensions-${{ matrix.php-version }}-${{ matrix.command }} - name: Cache extensions uses: actions/cache@v2 with: diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index 6ff6ec15..88068a16 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -12,15 +12,30 @@ jobs: matrix: php-version: ['8.1'] swoole: ['yes', 'no'] + env: + extensions: openswoole-4.11.1 steps: - name: Checkout code uses: actions/checkout@v2 + - name: Setup cache environment + id: extcache + uses: shivammathur/cache-extensions@v1 + with: + php-version: ${{ matrix.php-version }} + extensions: ${{ env.extensions }} + key: publish-release-extensions-${{ matrix.php-version }}-${{ matrix.swoole }} + - name: Cache extensions + uses: actions/cache@v2 + with: + path: ${{ steps.extcache.outputs.dir }} + key: ${{ steps.extcache.outputs.key }} + restore-keys: ${{ steps.extcache.outputs.key }} - name: Use PHP uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php-version }} tools: composer - extensions: openswoole-4.11.1 + extensions: ${{ env.extensions }} - if: ${{ matrix.swoole == 'yes' }} run: ./build.sh ${GITHUB_REF#refs/tags/v} - if: ${{ matrix.swoole == 'no' }} diff --git a/.github/workflows/publish-swagger-spec.yml b/.github/workflows/publish-swagger-spec.yml index 83864389..f78abb2a 100644 --- a/.github/workflows/publish-swagger-spec.yml +++ b/.github/workflows/publish-swagger-spec.yml @@ -11,6 +11,8 @@ jobs: strategy: matrix: php-version: ['8.1'] + env: + extensions: openswoole-4.11.1 steps: - name: Checkout code uses: actions/checkout@v2 @@ -18,12 +20,25 @@ jobs: id: determine_version run: echo "::set-output name=version::${GITHUB_REF#refs/tags/}" shell: bash + - name: Setup cache environment + id: extcache + uses: shivammathur/cache-extensions@v1 + with: + php-version: ${{ matrix.php-version }} + extensions: ${{ env.extensions }} + key: publish-release-extensions-${{ matrix.php-version }} + - name: Cache extensions + uses: actions/cache@v2 + with: + path: ${{ steps.extcache.outputs.dir }} + key: ${{ steps.extcache.outputs.key }} + restore-keys: ${{ steps.extcache.outputs.key }} - name: Use PHP uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php-version }} tools: composer - extensions: openswoole-4.11.1 + extensions: ${{ env.extensions }} coverage: none - run: composer install --no-interaction --prefer-dist - run: composer swagger:inline From 0c2bcaee342dac8cf35251fe9a5f0b2c07cd34b0 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 13 Aug 2022 10:12:52 +0200 Subject: [PATCH 019/108] Fixed typo --- .github/workflows/publish-swagger-spec.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish-swagger-spec.yml b/.github/workflows/publish-swagger-spec.yml index f78abb2a..8d9a5d8d 100644 --- a/.github/workflows/publish-swagger-spec.yml +++ b/.github/workflows/publish-swagger-spec.yml @@ -26,7 +26,7 @@ jobs: with: php-version: ${{ matrix.php-version }} extensions: ${{ env.extensions }} - key: publish-release-extensions-${{ matrix.php-version }} + key: publish-swagger-spec-extensions-${{ matrix.php-version }} - name: Cache extensions uses: actions/cache@v2 with: From de71821759bc046d644ea8d0b8dc49003da31145 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 13 Aug 2022 10:19:38 +0200 Subject: [PATCH 020/108] Updated to latest actions/checkout version --- .github/workflows/ci-db-tests.yml | 2 +- .github/workflows/ci-mutation-tests.yml | 2 +- .github/workflows/ci-tests.yml | 2 +- .github/workflows/ci.yml | 6 +++--- .github/workflows/docker-image-build.yml | 2 +- .github/workflows/publish-release.yml | 4 ++-- .github/workflows/publish-swagger-spec.yml | 2 +- 7 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci-db-tests.yml b/.github/workflows/ci-db-tests.yml index 234167d1..a8fb3d15 100644 --- a/.github/workflows/ci-db-tests.yml +++ b/.github/workflows/ci-db-tests.yml @@ -19,7 +19,7 @@ jobs: extensions: openswoole-4.11.1, pdo_sqlsrv-5.10.1 steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Install MSSQL ODBC if: ${{ inputs.platform == 'ms' }} run: sudo ./data/infra/ci/install-ms-odbc.sh diff --git a/.github/workflows/ci-mutation-tests.yml b/.github/workflows/ci-mutation-tests.yml index 85ff6a8e..bc985f12 100644 --- a/.github/workflows/ci-mutation-tests.yml +++ b/.github/workflows/ci-mutation-tests.yml @@ -18,7 +18,7 @@ jobs: extensions: openswoole-4.11.1 steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Setup cache environment id: extcache uses: shivammathur/cache-extensions@v1 diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml index 630c9614..157ebbed 100644 --- a/.github/workflows/ci-tests.yml +++ b/.github/workflows/ci-tests.yml @@ -18,7 +18,7 @@ jobs: extensions: openswoole-4.11.1 steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Start postgres database server if: ${{ inputs.test-group == 'api' }} run: docker-compose -f docker-compose.yml -f docker-compose.ci.yml up -d shlink_db_postgres diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2be038e2..eb2f3334 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,7 @@ jobs: extensions: openswoole-4.11.1 steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Setup cache environment id: extcache uses: shivammathur/cache-extensions@v1 @@ -124,7 +124,7 @@ jobs: php-version: ['8.1'] steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Use PHP uses: shivammathur/setup-php@v2 with: @@ -166,7 +166,7 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: fetch-depth: 100 - uses: marceloprado/has-changed-path@v1 diff --git a/.github/workflows/docker-image-build.yml b/.github/workflows/docker-image-build.yml index fb24e60b..a7581b5f 100644 --- a/.github/workflows/docker-image-build.yml +++ b/.github/workflows/docker-image-build.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Set up QEMU uses: docker/setup-qemu-action@v1 - name: Set up Docker Buildx diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index 88068a16..af6c2e26 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -16,7 +16,7 @@ jobs: extensions: openswoole-4.11.1 steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Setup cache environment id: extcache uses: shivammathur/cache-extensions@v1 @@ -50,7 +50,7 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v3 - uses: actions/download-artifact@v3 with: path: build diff --git a/.github/workflows/publish-swagger-spec.yml b/.github/workflows/publish-swagger-spec.yml index 8d9a5d8d..e0f9f2ee 100644 --- a/.github/workflows/publish-swagger-spec.yml +++ b/.github/workflows/publish-swagger-spec.yml @@ -15,7 +15,7 @@ jobs: extensions: openswoole-4.11.1 steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Determine version id: determine_version run: echo "::set-output name=version::${GITHUB_REF#refs/tags/}" From 759c0ea95790ab15b70bf1671475c591b2a3cdb3 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 13 Aug 2022 10:53:24 +0200 Subject: [PATCH 021/108] Extracted all steps for setting up to a reusable action --- .github/actions/ci-setup.yml | 46 +++++++++++++++++++++++++ .github/workflows/ci-mutation-tests.yml | 27 +++------------ 2 files changed, 50 insertions(+), 23 deletions(-) create mode 100644 .github/actions/ci-setup.yml diff --git a/.github/actions/ci-setup.yml b/.github/actions/ci-setup.yml new file mode 100644 index 00000000..077a55b2 --- /dev/null +++ b/.github/actions/ci-setup.yml @@ -0,0 +1,46 @@ +name: CI setup + +inputs: + install-deps: + type: boolean + required: true + default: true + php-version: + type: string + required: true + php-extensions: + type: string + required: true + extensions-cache-key: + type: string + required: true + +runs: + uses: composite + steps: + - name: Checkout code + uses: actions/checkout@v3 + - name: Setup cache environment + id: extcache + uses: shivammathur/cache-extensions@v1 + with: + php-version: ${{ inputs.php-version }} + extensions: ${{ inputs.extensions }} + key: ${{ inputs.extensions-cache-key }} + - name: Cache extensions + uses: actions/cache@v2 + with: + path: ${{ steps.extcache.outputs.dir }} + key: ${{ steps.extcache.outputs.key }} + restore-keys: ${{ steps.extcache.outputs.key }} + - name: Use PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ inputs.php-version }} + tools: composer + extensions: ${{ inputs.extensions }} + coverage: pcov + ini-values: pcov.directory=module + - name: Install dependencies + if: ${{ inputs.install-deps }} + run: composer install --no-interaction --prefer-dist diff --git a/.github/workflows/ci-mutation-tests.yml b/.github/workflows/ci-mutation-tests.yml index bc985f12..4f9acb2f 100644 --- a/.github/workflows/ci-mutation-tests.yml +++ b/.github/workflows/ci-mutation-tests.yml @@ -17,31 +17,12 @@ jobs: env: extensions: openswoole-4.11.1 steps: - - name: Checkout code - uses: actions/checkout@v3 - - name: Setup cache environment - id: extcache - uses: shivammathur/cache-extensions@v1 + - name: Setup + uses: './.github/actions/ci-setup.yml' with: php-version: ${{ matrix.php-version }} - extensions: ${{ env.extensions }} - key: mutation-tests-extensions-${{ matrix.php-version }}-${{ inputs.test-group }} - - name: Cache extensions - uses: actions/cache@v2 - with: - path: ${{ steps.extcache.outputs.dir }} - key: ${{ steps.extcache.outputs.key }} - restore-keys: ${{ steps.extcache.outputs.key }} - - name: Use PHP - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php-version }} - tools: composer - extensions: ${{ env.extensions }} - coverage: pcov - ini-values: pcov.directory=module - - name: Install dependencies - run: composer install --no-interaction --prefer-dist + php-extensions: ${{ env.extensions }} + extensions-cache-key: mutation-tests-extensions-${{ matrix.php-version }}-${{ inputs.test-group }} - uses: actions/download-artifact@v3 with: path: build From 9075d68b7cc889f2066a0c2d8e07c360b30ecba4 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 13 Aug 2022 11:03:40 +0200 Subject: [PATCH 022/108] Fixed reference to local composed action --- .github/actions/{ci-setup.yml => ci-setup/action.yml} | 0 .github/workflows/ci-mutation-tests.yml | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename .github/actions/{ci-setup.yml => ci-setup/action.yml} (100%) diff --git a/.github/actions/ci-setup.yml b/.github/actions/ci-setup/action.yml similarity index 100% rename from .github/actions/ci-setup.yml rename to .github/actions/ci-setup/action.yml diff --git a/.github/workflows/ci-mutation-tests.yml b/.github/workflows/ci-mutation-tests.yml index 4f9acb2f..62abad4e 100644 --- a/.github/workflows/ci-mutation-tests.yml +++ b/.github/workflows/ci-mutation-tests.yml @@ -18,7 +18,7 @@ jobs: extensions: openswoole-4.11.1 steps: - name: Setup - uses: './.github/actions/ci-setup.yml' + uses: './.github/actions/ci-setup' with: php-version: ${{ matrix.php-version }} php-extensions: ${{ env.extensions }} From fe4ced27095ea803d48a12f433b592e63d7d40da Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 13 Aug 2022 11:06:41 +0200 Subject: [PATCH 023/108] Moved checkout step back to workflow --- .github/actions/ci-setup/action.yml | 2 -- .github/workflows/ci-mutation-tests.yml | 1 + 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/actions/ci-setup/action.yml b/.github/actions/ci-setup/action.yml index 077a55b2..5340bf28 100644 --- a/.github/actions/ci-setup/action.yml +++ b/.github/actions/ci-setup/action.yml @@ -18,8 +18,6 @@ inputs: runs: uses: composite steps: - - name: Checkout code - uses: actions/checkout@v3 - name: Setup cache environment id: extcache uses: shivammathur/cache-extensions@v1 diff --git a/.github/workflows/ci-mutation-tests.yml b/.github/workflows/ci-mutation-tests.yml index 62abad4e..ff428d3e 100644 --- a/.github/workflows/ci-mutation-tests.yml +++ b/.github/workflows/ci-mutation-tests.yml @@ -17,6 +17,7 @@ jobs: env: extensions: openswoole-4.11.1 steps: + - uses: actions/checkout@v3 - name: Setup uses: './.github/actions/ci-setup' with: From ae2dc39a785a6df4cd89eea2f81af52cedfcc156 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 13 Aug 2022 11:16:33 +0200 Subject: [PATCH 024/108] Fixed ci-setup local composite action --- .github/actions/ci-setup/action.yml | 17 ++++++++++------- .github/workflows/ci-mutation-tests.yml | 7 ++----- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/actions/ci-setup/action.yml b/.github/actions/ci-setup/action.yml index 5340bf28..731f09d2 100644 --- a/.github/actions/ci-setup/action.yml +++ b/.github/actions/ci-setup/action.yml @@ -1,22 +1,24 @@ name: CI setup +description: 'Sets up the environment to run CI actions for Shlink' inputs: install-deps: - type: boolean + description: 'Tells if dependencies should be installed with composer. Default value is "yes"' required: true - default: true + default: 'yes' php-version: - type: string + description: 'The PHP version to be setup' required: true php-extensions: - type: string + description: 'The PHP extensions to install' required: true + default: '' extensions-cache-key: - type: string + description: 'The key used to cache PHP extensions. If empty value is provided, extension caching is disabled' required: true runs: - uses: composite + using: composite steps: - name: Setup cache environment id: extcache @@ -40,5 +42,6 @@ runs: coverage: pcov ini-values: pcov.directory=module - name: Install dependencies - if: ${{ inputs.install-deps }} + if: ${{ inputs.install-deps == 'yes' }} run: composer install --no-interaction --prefer-dist + shell: bash diff --git a/.github/workflows/ci-mutation-tests.yml b/.github/workflows/ci-mutation-tests.yml index ff428d3e..85b74f54 100644 --- a/.github/workflows/ci-mutation-tests.yml +++ b/.github/workflows/ci-mutation-tests.yml @@ -14,15 +14,12 @@ jobs: strategy: matrix: php-version: [ '8.1' ] - env: - extensions: openswoole-4.11.1 steps: - uses: actions/checkout@v3 - - name: Setup - uses: './.github/actions/ci-setup' + - uses: './.github/actions/ci-setup' with: php-version: ${{ matrix.php-version }} - php-extensions: ${{ env.extensions }} + php-extensions: openswoole-4.11.1 extensions-cache-key: mutation-tests-extensions-${{ matrix.php-version }}-${{ inputs.test-group }} - uses: actions/download-artifact@v3 with: From 763002ae14932368cc49c36992368ebc091a04c8 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 13 Aug 2022 11:18:50 +0200 Subject: [PATCH 025/108] Fixed typo when reading etxnesions input on ci-setup action --- .github/actions/ci-setup/action.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/actions/ci-setup/action.yml b/.github/actions/ci-setup/action.yml index 731f09d2..eb7c8979 100644 --- a/.github/actions/ci-setup/action.yml +++ b/.github/actions/ci-setup/action.yml @@ -25,7 +25,7 @@ runs: uses: shivammathur/cache-extensions@v1 with: php-version: ${{ inputs.php-version }} - extensions: ${{ inputs.extensions }} + extensions: ${{ inputs.php-extensions }} key: ${{ inputs.extensions-cache-key }} - name: Cache extensions uses: actions/cache@v2 @@ -38,7 +38,7 @@ runs: with: php-version: ${{ inputs.php-version }} tools: composer - extensions: ${{ inputs.extensions }} + extensions: ${{ inputs.php-extensions }} coverage: pcov ini-values: pcov.directory=module - name: Install dependencies From eb21833d94d375b06fa574f87d7fd69762a8c0ab Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 13 Aug 2022 11:56:46 +0200 Subject: [PATCH 026/108] Used ci-setup composite action as much as possible in ci workflow --- .github/workflows/ci-db-tests.yml | 28 +++------------------ .github/workflows/ci-tests.yml | 29 +++------------------- .github/workflows/ci.yml | 28 +++------------------ .github/workflows/publish-release.yml | 29 +++++----------------- .github/workflows/publish-swagger-spec.yml | 27 +++----------------- 5 files changed, 22 insertions(+), 119 deletions(-) diff --git a/.github/workflows/ci-db-tests.yml b/.github/workflows/ci-db-tests.yml index a8fb3d15..09543610 100644 --- a/.github/workflows/ci-db-tests.yml +++ b/.github/workflows/ci-db-tests.yml @@ -16,39 +16,19 @@ jobs: php-version: [ '8.1' ] env: LC_ALL: C - extensions: openswoole-4.11.1, pdo_sqlsrv-5.10.1 steps: - - name: Checkout code - uses: actions/checkout@v3 + - uses: actions/checkout@v3 - name: Install MSSQL ODBC if: ${{ inputs.platform == 'ms' }} run: sudo ./data/infra/ci/install-ms-odbc.sh - name: Start database server if: ${{ inputs.platform != 'sqlite:ci' }} run: docker-compose -f docker-compose.yml -f docker-compose.ci.yml up -d shlink_db_${{ inputs.platform }} - - name: Setup cache environment - id: extcache - uses: shivammathur/cache-extensions@v1 + - uses: './.github/actions/ci-setup' with: php-version: ${{ matrix.php-version }} - extensions: ${{ env.extensions }} - key: db-tests-extensions-${{ matrix.php-version }}-${{ inputs.platform }} - - name: Cache extensions - uses: actions/cache@v2 - with: - path: ${{ steps.extcache.outputs.dir }} - key: ${{ steps.extcache.outputs.key }} - restore-keys: ${{ steps.extcache.outputs.key }} - - name: Use PHP - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php-version }} - tools: composer - extensions: ${{ env.extensions }} - coverage: pcov - ini-values: pcov.directory=module - - name: Install dependencies - run: composer install --no-interaction --prefer-dist + php-extensions: openswoole-4.11.1, pdo_sqlsrv-5.10.1 + extensions-cache-key: db-tests-extensions-${{ matrix.php-version }}-${{ inputs.platform }} - name: Create test database if: ${{ inputs.platform == 'ms' }} run: docker-compose exec -T shlink_db_ms /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P 'Passw0rd!' -Q "CREATE DATABASE shlink_test;" diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml index 157ebbed..b7f7af98 100644 --- a/.github/workflows/ci-tests.yml +++ b/.github/workflows/ci-tests.yml @@ -14,40 +14,19 @@ jobs: strategy: matrix: php-version: ['8.1'] - env: - extensions: openswoole-4.11.1 steps: - - name: Checkout code - uses: actions/checkout@v3 + - uses: actions/checkout@v3 - name: Start postgres database server if: ${{ inputs.test-group == 'api' }} run: docker-compose -f docker-compose.yml -f docker-compose.ci.yml up -d shlink_db_postgres - name: Start maria database server if: ${{ inputs.test-group == 'cli' }} run: docker-compose -f docker-compose.yml -f docker-compose.ci.yml up -d shlink_db_maria - - name: Setup cache environment - id: extcache - uses: shivammathur/cache-extensions@v1 + - uses: './.github/actions/ci-setup' with: php-version: ${{ matrix.php-version }} - extensions: ${{ env.extensions }} - key: tests-extensions-${{ matrix.php-version }}-${{ inputs.test-group }} - - name: Cache extensions - uses: actions/cache@v2 - with: - path: ${{ steps.extcache.outputs.dir }} - key: ${{ steps.extcache.outputs.key }} - restore-keys: ${{ steps.extcache.outputs.key }} - - name: Use PHP - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php-version }} - tools: composer - extensions: ${{ env.extensions }} - coverage: pcov - ini-values: pcov.directory=module - - name: Install dependencies - run: composer install --no-interaction --prefer-dist + php-extensions: openswoole-4.11.1 + extensions-cache-key: tests-extensions-${{ matrix.php-version }}-${{ inputs.test-group }} - run: composer test:${{ inputs.test-group }}:ci - uses: actions/upload-artifact@v3 if: ${{ matrix.php-version == '8.1' }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eb2f3334..57985e8a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,33 +15,13 @@ jobs: matrix: php-version: ['8.1'] command: ['cs', 'stan', 'swagger:validate'] - env: - extensions: openswoole-4.11.1 steps: - - name: Checkout code - uses: actions/checkout@v3 - - name: Setup cache environment - id: extcache - uses: shivammathur/cache-extensions@v1 + - uses: actions/checkout@v3 + - uses: './.github/actions/ci-setup' with: php-version: ${{ matrix.php-version }} - extensions: ${{ env.extensions }} - key: tests-extensions-${{ matrix.php-version }}-${{ matrix.command }} - - name: Cache extensions - uses: actions/cache@v2 - with: - path: ${{ steps.extcache.outputs.dir }} - key: ${{ steps.extcache.outputs.key }} - restore-keys: ${{ steps.extcache.outputs.key }} - - name: Use PHP - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php-version }} - tools: composer - extensions: ${{ env.extensions }} - coverage: none - - name: Install dependencies - run: composer install --no-interaction --prefer-dist + php-extensions: openswoole-4.11.1 + extensions-cache-key: tests-extensions-${{ matrix.php-version }}-${{ matrix.command }} - run: composer ${{ matrix.command }} unit-tests: diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index af6c2e26..b4ed7bba 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -12,30 +12,14 @@ jobs: matrix: php-version: ['8.1'] swoole: ['yes', 'no'] - env: - extensions: openswoole-4.11.1 steps: - - name: Checkout code - uses: actions/checkout@v3 - - name: Setup cache environment - id: extcache - uses: shivammathur/cache-extensions@v1 + - uses: actions/checkout@v3 + - uses: './.github/actions/ci-setup' with: php-version: ${{ matrix.php-version }} - extensions: ${{ env.extensions }} - key: publish-release-extensions-${{ matrix.php-version }}-${{ matrix.swoole }} - - name: Cache extensions - uses: actions/cache@v2 - with: - path: ${{ steps.extcache.outputs.dir }} - key: ${{ steps.extcache.outputs.key }} - restore-keys: ${{ steps.extcache.outputs.key }} - - name: Use PHP - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php-version }} - tools: composer - extensions: ${{ env.extensions }} + php-extensions: openswoole-4.11.1 + extensions-cache-key: publish-swagger-spec-extensions-${{ matrix.php-version }} + install-deps: 'no' - if: ${{ matrix.swoole == 'yes' }} run: ./build.sh ${GITHUB_REF#refs/tags/v} - if: ${{ matrix.swoole == 'no' }} @@ -49,8 +33,7 @@ jobs: needs: ['build'] runs-on: ubuntu-22.04 steps: - - name: Checkout code - uses: actions/checkout@v3 + - uses: actions/checkout@v3 - uses: actions/download-artifact@v3 with: path: build diff --git a/.github/workflows/publish-swagger-spec.yml b/.github/workflows/publish-swagger-spec.yml index e0f9f2ee..9002353d 100644 --- a/.github/workflows/publish-swagger-spec.yml +++ b/.github/workflows/publish-swagger-spec.yml @@ -11,36 +11,17 @@ jobs: strategy: matrix: php-version: ['8.1'] - env: - extensions: openswoole-4.11.1 steps: - - name: Checkout code - uses: actions/checkout@v3 + - uses: actions/checkout@v3 - name: Determine version id: determine_version run: echo "::set-output name=version::${GITHUB_REF#refs/tags/}" shell: bash - - name: Setup cache environment - id: extcache - uses: shivammathur/cache-extensions@v1 + - uses: './.github/actions/ci-setup' with: php-version: ${{ matrix.php-version }} - extensions: ${{ env.extensions }} - key: publish-swagger-spec-extensions-${{ matrix.php-version }} - - name: Cache extensions - uses: actions/cache@v2 - with: - path: ${{ steps.extcache.outputs.dir }} - key: ${{ steps.extcache.outputs.key }} - restore-keys: ${{ steps.extcache.outputs.key }} - - name: Use PHP - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php-version }} - tools: composer - extensions: ${{ env.extensions }} - coverage: none - - run: composer install --no-interaction --prefer-dist + php-extensions: openswoole-4.11.1 + extensions-cache-key: publish-swagger-spec-extensions-${{ matrix.php-version }} - run: composer swagger:inline - run: mkdir ${{ steps.determine_version.outputs.version }} - run: mv docs/swagger/swagger-inlined.json ${{ steps.determine_version.outputs.version }}/open-api-spec.json From bd31b9932454a7299fd8736985490def1ddb0d8c Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 13 Aug 2022 12:31:12 +0200 Subject: [PATCH 027/108] Ensured only mutants for changed lines are executed in CI mutation tests --- .github/workflows/ci-mutation-tests.yml | 4 ++-- composer.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci-mutation-tests.yml b/.github/workflows/ci-mutation-tests.yml index 85b74f54..a4ce7432 100644 --- a/.github/workflows/ci-mutation-tests.yml +++ b/.github/workflows/ci-mutation-tests.yml @@ -25,8 +25,8 @@ jobs: with: path: build - if: ${{ inputs.test-group == 'unit' }} - run: composer infect:ci:unit + run: composer infect:ci:unit -- --git-diff-lines --logger-github=false env: INFECTION_BADGE_API_KEY: ${{ secrets.INFECTION_BADGE_API_KEY }} - if: ${{ inputs.test-group != 'unit' }} - run: composer infect:ci:${{ inputs.test-group }} + run: composer infect:ci:${{ inputs.test-group }} -- --git-diff-lines --logger-github=false diff --git a/composer.json b/composer.json index 0cc0e65c..ad11461b 100644 --- a/composer.json +++ b/composer.json @@ -125,7 +125,7 @@ "test:cli": "APP_ENV=test DB_DRIVER=maria TEST_ENV=cli php vendor/bin/phpunit --order-by=random --colors=always --testdox -c phpunit-cli.xml --log-junit=build/coverage-cli/junit.xml", "test:cli:ci": "GENERATE_COVERAGE=yes composer test:cli", "test:cli:pretty": "GENERATE_COVERAGE=pretty composer test:cli", - "infect:ci:base": "infection --threads=4 --log-verbosity=default --only-covered --only-covering-test-cases --skip-initial-tests", + "infect:ci:base": "infection --threads=4 --only-covered --only-covering-test-cases --skip-initial-tests", "infect:ci:unit": "@infect:ci:base --coverage=build/coverage-unit --min-msi=84", "infect:ci:db": "@infect:ci:base --coverage=build/coverage-db --min-msi=95 --configuration=infection-db.json", "infect:ci:api": "@infect:ci:base --coverage=build/coverage-api --min-msi=80 --configuration=infection-api.json", From ed7be6eb9994598449eaeec6086e1821ad189440 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 13 Aug 2022 12:37:15 +0200 Subject: [PATCH 028/108] Updated changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 93701f61..202e1ba2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this ### Changed * [#1339](https://github.com/shlinkio/shlink/issues/1339) Added new test suite for CLI E2E tests. +* [#1503](https://github.com/shlinkio/shlink/issues/1503) Drastically improved build time in GitHub Actions, by optimizing parallelization, adding php extensions cache and running mutation tests only for changed files. ### Deprecated * *Nothing* From cd4fe4362bcb325e665dfbc537c1c9eb8ea42b0d Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 13 Aug 2022 16:50:19 +0200 Subject: [PATCH 029/108] Created middleware to keep backwards compatibility on errors when using v1 and 2 of the API --- .../autoload/middleware-pipeline.global.php | 1 + .../src/Exception/ValidationException.php | 2 +- module/Rest/config/dependencies.config.php | 1 + ...wardsCompatibleProblemDetailsException.php | 74 ++++++++++++++++++ ...ckwardsCompatibleProblemDetailsHandler.php | 30 ++++++++ ...rdsCompatibleProblemDetailsHandlerTest.php | 76 +++++++++++++++++++ 6 files changed, 183 insertions(+), 1 deletion(-) create mode 100644 module/Rest/src/Exception/BackwardsCompatibleProblemDetailsException.php create mode 100644 module/Rest/src/Middleware/ErrorHandler/BackwardsCompatibleProblemDetailsHandler.php create mode 100644 module/Rest/test/Middleware/ErrorHandler/BackwardsCompatibleProblemDetailsHandlerTest.php diff --git a/config/autoload/middleware-pipeline.global.php b/config/autoload/middleware-pipeline.global.php index c628c4fd..5291db8c 100644 --- a/config/autoload/middleware-pipeline.global.php +++ b/config/autoload/middleware-pipeline.global.php @@ -26,6 +26,7 @@ return [ 'path' => '/rest', 'middleware' => [ ProblemDetails\ProblemDetailsMiddleware::class, + Rest\Middleware\ErrorHandler\BackwardsCompatibleProblemDetailsHandler::class, ], ], diff --git a/module/Core/src/Exception/ValidationException.php b/module/Core/src/Exception/ValidationException.php index 326eec11..0f625bc5 100644 --- a/module/Core/src/Exception/ValidationException.php +++ b/module/Core/src/Exception/ValidationException.php @@ -21,7 +21,7 @@ class ValidationException extends InvalidArgumentException implements ProblemDet use CommonProblemDetailsExceptionTrait; private const TITLE = 'Invalid data'; - private const TYPE = 'INVALID_ARGUMENT'; + public const TYPE = 'https://shlink.io/api/error/invalid-data'; private array $invalidElements; diff --git a/module/Rest/config/dependencies.config.php b/module/Rest/config/dependencies.config.php index 189180b0..a70cb7f1 100644 --- a/module/Rest/config/dependencies.config.php +++ b/module/Rest/config/dependencies.config.php @@ -53,6 +53,7 @@ return [ Middleware\ShortUrl\DefaultShortCodesLengthMiddleware::class => ConfigAbstractFactory::class, Middleware\ShortUrl\OverrideDomainMiddleware::class => ConfigAbstractFactory::class, Middleware\Mercure\NotConfiguredMercureErrorHandler::class => ConfigAbstractFactory::class, + Middleware\ErrorHandler\BackwardsCompatibleProblemDetailsHandler::class => InvokableFactory::class, ], ], diff --git a/module/Rest/src/Exception/BackwardsCompatibleProblemDetailsException.php b/module/Rest/src/Exception/BackwardsCompatibleProblemDetailsException.php new file mode 100644 index 00000000..ddc3768f --- /dev/null +++ b/module/Rest/src/Exception/BackwardsCompatibleProblemDetailsException.php @@ -0,0 +1,74 @@ +getMessage(), $e->getCode(), $e); + } + + public static function fromProblemDetails(ProblemDetailsExceptionInterface $e): self + { + return new self($e); + } + + public function getStatus(): int + { + return $this->e->getStatus(); + } + + public function getType(): string + { + return $this->remapType($this->e->getType()); + } + + public function getTitle(): string + { + return $this->e->getTitle(); + } + + public function getDetail(): string + { + return $this->e->getDetail(); + } + + public function getAdditionalData(): array + { + return $this->e->getAdditionalData(); + } + + public function toArray(): array + { + return $this->remapTypeInArray($this->e->toArray()); + } + + public function jsonSerialize(): array + { + return $this->remapTypeInArray($this->e->jsonSerialize()); + } + + private function remapTypeInArray(array $wrappedArray): array + { + if (! isset($wrappedArray['type'])) { + return $wrappedArray; + } + + return [...$wrappedArray, 'type' => $this->remapType($wrappedArray['type'])]; + } + + private function remapType(string $wrappedType): string + { + return match ($wrappedType) { + ValidationException::TYPE => 'INVALID_ARGUMENT', + default => $wrappedType, + }; + } +} diff --git a/module/Rest/src/Middleware/ErrorHandler/BackwardsCompatibleProblemDetailsHandler.php b/module/Rest/src/Middleware/ErrorHandler/BackwardsCompatibleProblemDetailsHandler.php new file mode 100644 index 00000000..c099ad70 --- /dev/null +++ b/module/Rest/src/Middleware/ErrorHandler/BackwardsCompatibleProblemDetailsHandler.php @@ -0,0 +1,30 @@ +handle($request); + } catch (ProblemDetailsExceptionInterface $e) { + $version = $request->getAttribute('version') ?? '2'; + throw version_compare($version, '3', '>=') + ? $e + : BackwardsCompatibleProblemDetailsException::fromProblemDetails($e); + } + } +} diff --git a/module/Rest/test/Middleware/ErrorHandler/BackwardsCompatibleProblemDetailsHandlerTest.php b/module/Rest/test/Middleware/ErrorHandler/BackwardsCompatibleProblemDetailsHandlerTest.php new file mode 100644 index 00000000..00dddb2f --- /dev/null +++ b/module/Rest/test/Middleware/ErrorHandler/BackwardsCompatibleProblemDetailsHandlerTest.php @@ -0,0 +1,76 @@ +handler = new BackwardsCompatibleProblemDetailsHandler(); + } + + /** + * @test + * @dataProvider provideExceptions + */ + public function expectedExceptionIsThrownBasedOnTheRequestVersion( + ServerRequestInterface $request, + Throwable $thrownException, + string $expectedException, + ): void { + $handler = $this->prophesize(RequestHandlerInterface::class); + $handle = $handler->handle($request)->willThrow($thrownException); + + $this->expectException($expectedException); + $handle->shouldBeCalledOnce(); + + $this->handler->process($request, $handler->reveal()); + } + + public function provideExceptions(): iterable + { + $baseRequest = ServerRequestFactory::fromGlobals(); + + yield 'no version' => [ + $baseRequest, + ValidationException::fromArray([]), + BackwardsCompatibleProblemDetailsException::class, + ]; + yield 'version 1' => [ + $baseRequest->withAttribute('version', '1'), + ValidationException::fromArray([]), + BackwardsCompatibleProblemDetailsException::class, + ]; + yield 'version 2' => [ + $baseRequest->withAttribute('version', '2'), + ValidationException::fromArray([]), + BackwardsCompatibleProblemDetailsException::class, + ]; + yield 'version 3' => [ + $baseRequest->withAttribute('version', '3'), + ValidationException::fromArray([]), + ValidationException::class, + ]; + yield 'version 4' => [ + $baseRequest->withAttribute('version', '3'), + ValidationException::fromArray([]), + ValidationException::class, + ]; + } +} From 905f51fbd0583a587ecf6a63ad18a18b3fc35cf4 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 13 Aug 2022 17:15:04 +0200 Subject: [PATCH 030/108] Added logic to properly map all existing errors from v3 to v2 in the API --- composer.json | 2 +- .../src/Exception/DeleteShortUrlException.php | 2 +- .../src/Exception/DomainNotFoundException.php | 2 +- .../ForbiddenTagOperationException.php | 2 +- .../src/Exception/InvalidUrlException.php | 2 +- .../src/Exception/NonUniqueSlugException.php | 2 +- .../Exception/ShortUrlNotFoundException.php | 2 +- .../src/Exception/TagConflictException.php | 2 +- .../src/Exception/TagNotFoundException.php | 2 +- .../Exception/DeleteShortUrlExceptionTest.php | 2 +- .../Exception/DomainNotFoundExceptionTest.php | 4 ++-- .../ForbiddenTagOperationExceptionTest.php | 2 +- .../Exception/InvalidUrlExceptionTest.php | 2 +- .../Exception/NonUniqueSlugExceptionTest.php | 2 +- .../ShortUrlNotFoundExceptionTest.php | 2 +- .../Exception/TagConflictExceptionTest.php | 2 +- .../Exception/TagNotFoundExceptionTest.php | 2 +- module/Rest/src/ConfigProvider.php | 2 +- ...wardsCompatibleProblemDetailsException.php | 19 +++++++++++++++++++ .../Rest/src/Exception/MercureException.php | 2 +- .../MissingAuthenticationException.php | 2 +- .../VerifyAuthenticationException.php | 4 +++- module/Rest/test/ConfigProviderTest.php | 14 +++++++------- .../MissingAuthenticationExceptionTest.php | 2 +- 24 files changed, 51 insertions(+), 30 deletions(-) diff --git a/composer.json b/composer.json index ad11461b..168418a5 100644 --- a/composer.json +++ b/composer.json @@ -69,7 +69,7 @@ "phpunit/phpunit": "^9.5", "roave/security-advisories": "dev-master", "shlinkio/php-coding-standard": "~2.3.0", - "shlinkio/shlink-test-utils": "^3.1.0", + "shlinkio/shlink-test-utils": "^3.2", "symfony/var-dumper": "^6.1", "veewee/composer-run-parallel": "^1.1" }, diff --git a/module/Core/src/Exception/DeleteShortUrlException.php b/module/Core/src/Exception/DeleteShortUrlException.php index 0d331400..f6638221 100644 --- a/module/Core/src/Exception/DeleteShortUrlException.php +++ b/module/Core/src/Exception/DeleteShortUrlException.php @@ -16,7 +16,7 @@ class DeleteShortUrlException extends DomainException implements ProblemDetailsE use CommonProblemDetailsExceptionTrait; private const TITLE = 'Cannot delete short URL'; - private const TYPE = 'INVALID_SHORT_URL_DELETION'; + public const TYPE = 'https://shlink.io/api/error/invalid-short-url-deletion'; public static function fromVisitsThreshold(int $threshold, ShortUrlIdentifier $identifier): self { diff --git a/module/Core/src/Exception/DomainNotFoundException.php b/module/Core/src/Exception/DomainNotFoundException.php index cb19608a..aca67813 100644 --- a/module/Core/src/Exception/DomainNotFoundException.php +++ b/module/Core/src/Exception/DomainNotFoundException.php @@ -15,7 +15,7 @@ class DomainNotFoundException extends DomainException implements ProblemDetailsE use CommonProblemDetailsExceptionTrait; private const TITLE = 'Domain not found'; - private const TYPE = 'DOMAIN_NOT_FOUND'; + public const TYPE = 'https://shlink.io/api/error/domain-not-found'; private function __construct(string $message, array $additional) { diff --git a/module/Core/src/Exception/ForbiddenTagOperationException.php b/module/Core/src/Exception/ForbiddenTagOperationException.php index d4200c92..6da1cb61 100644 --- a/module/Core/src/Exception/ForbiddenTagOperationException.php +++ b/module/Core/src/Exception/ForbiddenTagOperationException.php @@ -13,7 +13,7 @@ class ForbiddenTagOperationException extends DomainException implements ProblemD use CommonProblemDetailsExceptionTrait; private const TITLE = 'Forbidden tag operation'; - private const TYPE = 'FORBIDDEN_OPERATION'; + public const TYPE = 'https://shlink.io/api/error/forbidden-tag-operation'; public static function forDeletion(): self { diff --git a/module/Core/src/Exception/InvalidUrlException.php b/module/Core/src/Exception/InvalidUrlException.php index ee4caaf6..6d1f93c6 100644 --- a/module/Core/src/Exception/InvalidUrlException.php +++ b/module/Core/src/Exception/InvalidUrlException.php @@ -16,7 +16,7 @@ class InvalidUrlException extends DomainException implements ProblemDetailsExcep use CommonProblemDetailsExceptionTrait; private const TITLE = 'Invalid URL'; - private const TYPE = 'INVALID_URL'; + public const TYPE = 'https://shlink.io/api/error/invalid-url'; public static function fromUrl(string $url, ?Throwable $previous = null): self { diff --git a/module/Core/src/Exception/NonUniqueSlugException.php b/module/Core/src/Exception/NonUniqueSlugException.php index f61c480f..dc1fcca9 100644 --- a/module/Core/src/Exception/NonUniqueSlugException.php +++ b/module/Core/src/Exception/NonUniqueSlugException.php @@ -16,7 +16,7 @@ class NonUniqueSlugException extends InvalidArgumentException implements Problem use CommonProblemDetailsExceptionTrait; private const TITLE = 'Invalid custom slug'; - private const TYPE = 'INVALID_SLUG'; + public const TYPE = 'https://shlink.io/api/error/non-unique-slug'; public static function fromSlug(string $slug, ?string $domain = null): self { diff --git a/module/Core/src/Exception/ShortUrlNotFoundException.php b/module/Core/src/Exception/ShortUrlNotFoundException.php index c59c20ef..4da6972e 100644 --- a/module/Core/src/Exception/ShortUrlNotFoundException.php +++ b/module/Core/src/Exception/ShortUrlNotFoundException.php @@ -16,7 +16,7 @@ class ShortUrlNotFoundException extends DomainException implements ProblemDetail use CommonProblemDetailsExceptionTrait; private const TITLE = 'Short URL not found'; - private const TYPE = 'INVALID_SHORTCODE'; + public const TYPE = 'https://shlink.io/api/error/short-url-not-found'; public static function fromNotFound(ShortUrlIdentifier $identifier): self { diff --git a/module/Core/src/Exception/TagConflictException.php b/module/Core/src/Exception/TagConflictException.php index d551ec19..09ea7be4 100644 --- a/module/Core/src/Exception/TagConflictException.php +++ b/module/Core/src/Exception/TagConflictException.php @@ -16,7 +16,7 @@ class TagConflictException extends RuntimeException implements ProblemDetailsExc use CommonProblemDetailsExceptionTrait; private const TITLE = 'Tag conflict'; - private const TYPE = 'TAG_CONFLICT'; + public const TYPE = 'https://shlink.io/api/error/tag-conflict'; public static function forExistingTag(TagRenaming $renaming): self { diff --git a/module/Core/src/Exception/TagNotFoundException.php b/module/Core/src/Exception/TagNotFoundException.php index 18c1554c..da2426aa 100644 --- a/module/Core/src/Exception/TagNotFoundException.php +++ b/module/Core/src/Exception/TagNotFoundException.php @@ -15,7 +15,7 @@ class TagNotFoundException extends DomainException implements ProblemDetailsExce use CommonProblemDetailsExceptionTrait; private const TITLE = 'Tag not found'; - private const TYPE = 'TAG_NOT_FOUND'; + public const TYPE = 'https://shlink.io/api/error/tag-not-found'; public static function fromTag(string $tag): self { diff --git a/module/Core/test/Exception/DeleteShortUrlExceptionTest.php b/module/Core/test/Exception/DeleteShortUrlExceptionTest.php index b331bdc2..e658e55d 100644 --- a/module/Core/test/Exception/DeleteShortUrlExceptionTest.php +++ b/module/Core/test/Exception/DeleteShortUrlExceptionTest.php @@ -37,7 +37,7 @@ class DeleteShortUrlExceptionTest extends TestCase 'threshold' => $threshold, ], $e->getAdditionalData()); self::assertEquals('Cannot delete short URL', $e->getTitle()); - self::assertEquals('INVALID_SHORT_URL_DELETION', $e->getType()); + self::assertEquals('https://shlink.io/api/error/invalid-short-url-deletion', $e->getType()); self::assertEquals(422, $e->getStatus()); } diff --git a/module/Core/test/Exception/DomainNotFoundExceptionTest.php b/module/Core/test/Exception/DomainNotFoundExceptionTest.php index 5f2b9889..f2f5daba 100644 --- a/module/Core/test/Exception/DomainNotFoundExceptionTest.php +++ b/module/Core/test/Exception/DomainNotFoundExceptionTest.php @@ -21,7 +21,7 @@ class DomainNotFoundExceptionTest extends TestCase self::assertEquals($expectedMessage, $e->getMessage()); self::assertEquals($expectedMessage, $e->getDetail()); self::assertEquals('Domain not found', $e->getTitle()); - self::assertEquals('DOMAIN_NOT_FOUND', $e->getType()); + self::assertEquals('https://shlink.io/api/error/domain-not-found', $e->getType()); self::assertEquals(['id' => $id], $e->getAdditionalData()); self::assertEquals(404, $e->getStatus()); } @@ -36,7 +36,7 @@ class DomainNotFoundExceptionTest extends TestCase self::assertEquals($expectedMessage, $e->getMessage()); self::assertEquals($expectedMessage, $e->getDetail()); self::assertEquals('Domain not found', $e->getTitle()); - self::assertEquals('DOMAIN_NOT_FOUND', $e->getType()); + self::assertEquals('https://shlink.io/api/error/domain-not-found', $e->getType()); self::assertEquals(['authority' => $authority], $e->getAdditionalData()); self::assertEquals(404, $e->getStatus()); } diff --git a/module/Core/test/Exception/ForbiddenTagOperationExceptionTest.php b/module/Core/test/Exception/ForbiddenTagOperationExceptionTest.php index 40ccd0ee..b064cf91 100644 --- a/module/Core/test/Exception/ForbiddenTagOperationExceptionTest.php +++ b/module/Core/test/Exception/ForbiddenTagOperationExceptionTest.php @@ -25,7 +25,7 @@ class ForbiddenTagOperationExceptionTest extends TestCase self::assertEquals($expectedMessage, $e->getMessage()); self::assertEquals($expectedMessage, $e->getDetail()); self::assertEquals('Forbidden tag operation', $e->getTitle()); - self::assertEquals('FORBIDDEN_OPERATION', $e->getType()); + self::assertEquals('https://shlink.io/api/error/forbidden-tag-operation', $e->getType()); self::assertEquals(403, $e->getStatus()); } diff --git a/module/Core/test/Exception/InvalidUrlExceptionTest.php b/module/Core/test/Exception/InvalidUrlExceptionTest.php index 5351c1b3..e9b0d75a 100644 --- a/module/Core/test/Exception/InvalidUrlExceptionTest.php +++ b/module/Core/test/Exception/InvalidUrlExceptionTest.php @@ -27,7 +27,7 @@ class InvalidUrlExceptionTest extends TestCase self::assertEquals($expectedMessage, $e->getMessage()); self::assertEquals($expectedMessage, $e->getDetail()); self::assertEquals('Invalid URL', $e->getTitle()); - self::assertEquals('INVALID_URL', $e->getType()); + self::assertEquals('https://shlink.io/api/error/invalid-url', $e->getType()); self::assertEquals(['url' => $url], $e->getAdditionalData()); self::assertEquals(StatusCodeInterface::STATUS_BAD_REQUEST, $e->getCode()); self::assertEquals(StatusCodeInterface::STATUS_BAD_REQUEST, $e->getStatus()); diff --git a/module/Core/test/Exception/NonUniqueSlugExceptionTest.php b/module/Core/test/Exception/NonUniqueSlugExceptionTest.php index 6720f0f3..77a71df3 100644 --- a/module/Core/test/Exception/NonUniqueSlugExceptionTest.php +++ b/module/Core/test/Exception/NonUniqueSlugExceptionTest.php @@ -25,7 +25,7 @@ class NonUniqueSlugExceptionTest extends TestCase self::assertEquals($expectedMessage, $e->getMessage()); self::assertEquals($expectedMessage, $e->getDetail()); self::assertEquals('Invalid custom slug', $e->getTitle()); - self::assertEquals('INVALID_SLUG', $e->getType()); + self::assertEquals('https://shlink.io/api/error/non-unique-slug', $e->getType()); self::assertEquals(400, $e->getStatus()); self::assertEquals($expectedAdditional, $e->getAdditionalData()); } diff --git a/module/Core/test/Exception/ShortUrlNotFoundExceptionTest.php b/module/Core/test/Exception/ShortUrlNotFoundExceptionTest.php index e86a63cb..2818f350 100644 --- a/module/Core/test/Exception/ShortUrlNotFoundExceptionTest.php +++ b/module/Core/test/Exception/ShortUrlNotFoundExceptionTest.php @@ -29,7 +29,7 @@ class ShortUrlNotFoundExceptionTest extends TestCase self::assertEquals($expectedMessage, $e->getMessage()); self::assertEquals($expectedMessage, $e->getDetail()); self::assertEquals('Short URL not found', $e->getTitle()); - self::assertEquals('INVALID_SHORTCODE', $e->getType()); + self::assertEquals('https://shlink.io/api/error/short-url-not-found', $e->getType()); self::assertEquals(404, $e->getStatus()); self::assertEquals($expectedAdditional, $e->getAdditionalData()); } diff --git a/module/Core/test/Exception/TagConflictExceptionTest.php b/module/Core/test/Exception/TagConflictExceptionTest.php index 4427eb40..ba7dfa1d 100644 --- a/module/Core/test/Exception/TagConflictExceptionTest.php +++ b/module/Core/test/Exception/TagConflictExceptionTest.php @@ -23,7 +23,7 @@ class TagConflictExceptionTest extends TestCase self::assertEquals($expectedMessage, $e->getMessage()); self::assertEquals($expectedMessage, $e->getDetail()); self::assertEquals('Tag conflict', $e->getTitle()); - self::assertEquals('TAG_CONFLICT', $e->getType()); + self::assertEquals('https://shlink.io/api/error/tag-conflict', $e->getType()); self::assertEquals(['oldName' => $oldName, 'newName' => $newName], $e->getAdditionalData()); self::assertEquals(409, $e->getStatus()); } diff --git a/module/Core/test/Exception/TagNotFoundExceptionTest.php b/module/Core/test/Exception/TagNotFoundExceptionTest.php index ccd63788..f22463c2 100644 --- a/module/Core/test/Exception/TagNotFoundExceptionTest.php +++ b/module/Core/test/Exception/TagNotFoundExceptionTest.php @@ -21,7 +21,7 @@ class TagNotFoundExceptionTest extends TestCase self::assertEquals($expectedMessage, $e->getMessage()); self::assertEquals($expectedMessage, $e->getDetail()); self::assertEquals('Tag not found', $e->getTitle()); - self::assertEquals('TAG_NOT_FOUND', $e->getType()); + self::assertEquals('https://shlink.io/api/error/tag-not-found', $e->getType()); self::assertEquals(['tag' => $tag], $e->getAdditionalData()); self::assertEquals(404, $e->getStatus()); } diff --git a/module/Rest/src/ConfigProvider.php b/module/Rest/src/ConfigProvider.php index 6d389038..215a4d6e 100644 --- a/module/Rest/src/ConfigProvider.php +++ b/module/Rest/src/ConfigProvider.php @@ -11,7 +11,7 @@ use function sprintf; class ConfigProvider { - private const ROUTES_PREFIX = '/rest/v{version:1|2}'; + private const ROUTES_PREFIX = '/rest/v{version:1|2|3}'; private const UNVERSIONED_ROUTES_PREFIX = '/rest'; public const UNVERSIONED_HEALTH_ENDPOINT_NAME = 'unversioned_health'; diff --git a/module/Rest/src/Exception/BackwardsCompatibleProblemDetailsException.php b/module/Rest/src/Exception/BackwardsCompatibleProblemDetailsException.php index ddc3768f..14a6e934 100644 --- a/module/Rest/src/Exception/BackwardsCompatibleProblemDetailsException.php +++ b/module/Rest/src/Exception/BackwardsCompatibleProblemDetailsException.php @@ -5,6 +5,14 @@ declare(strict_types=1); namespace Shlinkio\Shlink\Rest\Exception; use Mezzio\ProblemDetails\Exception\ProblemDetailsExceptionInterface; +use Shlinkio\Shlink\Core\Exception\DeleteShortUrlException; +use Shlinkio\Shlink\Core\Exception\DomainNotFoundException; +use Shlinkio\Shlink\Core\Exception\ForbiddenTagOperationException; +use Shlinkio\Shlink\Core\Exception\InvalidUrlException; +use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException; +use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException; +use Shlinkio\Shlink\Core\Exception\TagConflictException; +use Shlinkio\Shlink\Core\Exception\TagNotFoundException; use Shlinkio\Shlink\Core\Exception\ValidationException; /** @deprecated */ @@ -68,6 +76,17 @@ class BackwardsCompatibleProblemDetailsException extends RuntimeException implem { return match ($wrappedType) { ValidationException::TYPE => 'INVALID_ARGUMENT', + DeleteShortUrlException::TYPE => 'INVALID_SHORT_URL_DELETION', + DomainNotFoundException::TYPE => 'DOMAIN_NOT_FOUND', + ForbiddenTagOperationException::TYPE => 'FORBIDDEN_OPERATION', + InvalidUrlException::TYPE => 'INVALID_URL', + NonUniqueSlugException::TYPE => 'INVALID_SLUG', + ShortUrlNotFoundException::TYPE => 'INVALID_SHORTCODE', + TagConflictException::TYPE => 'TAG_CONFLICT', + TagNotFoundException::TYPE => 'TAG_NOT_FOUND', + MercureException::TYPE => 'MERCURE_NOT_CONFIGURED', + MissingAuthenticationException::TYPE => 'INVALID_AUTHORIZATION', + VerifyAuthenticationException::TYPE => 'INVALID_API_KEY', default => $wrappedType, }; } diff --git a/module/Rest/src/Exception/MercureException.php b/module/Rest/src/Exception/MercureException.php index 9435cb54..0e9a6edb 100644 --- a/module/Rest/src/Exception/MercureException.php +++ b/module/Rest/src/Exception/MercureException.php @@ -13,7 +13,7 @@ class MercureException extends RuntimeException implements ProblemDetailsExcepti use CommonProblemDetailsExceptionTrait; private const TITLE = 'Mercure integration not configured'; - private const TYPE = 'MERCURE_NOT_CONFIGURED'; + public const TYPE = 'https://shlink.io/api/error/mercure-not-configured'; public static function mercureNotConfigured(): self { diff --git a/module/Rest/src/Exception/MissingAuthenticationException.php b/module/Rest/src/Exception/MissingAuthenticationException.php index 99dbc0df..3c1f5b9f 100644 --- a/module/Rest/src/Exception/MissingAuthenticationException.php +++ b/module/Rest/src/Exception/MissingAuthenticationException.php @@ -16,7 +16,7 @@ class MissingAuthenticationException extends RuntimeException implements Problem use CommonProblemDetailsExceptionTrait; private const TITLE = 'Invalid authorization'; - private const TYPE = 'INVALID_AUTHORIZATION'; + public const TYPE = 'https://shlink.io/api/error/missing-authentication'; public static function forHeaders(array $expectedHeaders): self { diff --git a/module/Rest/src/Exception/VerifyAuthenticationException.php b/module/Rest/src/Exception/VerifyAuthenticationException.php index 702230ff..25541d03 100644 --- a/module/Rest/src/Exception/VerifyAuthenticationException.php +++ b/module/Rest/src/Exception/VerifyAuthenticationException.php @@ -12,13 +12,15 @@ class VerifyAuthenticationException extends RuntimeException implements ProblemD { use CommonProblemDetailsExceptionTrait; + public const TYPE = 'https://shlink.io/api/error/invalid-api-key'; + public static function forInvalidApiKey(): self { $e = new self('Provided API key does not exist or is invalid.'); $e->detail = $e->getMessage(); $e->title = 'Invalid API key'; - $e->type = 'INVALID_API_KEY'; + $e->type = self::TYPE; $e->status = StatusCodeInterface::STATUS_UNAUTHORIZED; return $e; diff --git a/module/Rest/test/ConfigProviderTest.php b/module/Rest/test/ConfigProviderTest.php index a3f7d0c9..d3288151 100644 --- a/module/Rest/test/ConfigProviderTest.php +++ b/module/Rest/test/ConfigProviderTest.php @@ -48,10 +48,10 @@ class ConfigProviderTest extends TestCase ['path' => '/health'], ], [ - ['path' => '/rest/v{version:1|2}/foo'], - ['path' => '/rest/v{version:1|2}/bar'], - ['path' => '/rest/v{version:1|2}/baz/foo'], - ['path' => '/rest/v{version:1|2}/health'], + ['path' => '/rest/v{version:1|2|3}/foo'], + ['path' => '/rest/v{version:1|2|3}/bar'], + ['path' => '/rest/v{version:1|2|3}/baz/foo'], + ['path' => '/rest/v{version:1|2|3}/health'], ['path' => '/rest/health', 'name' => ConfigProvider::UNVERSIONED_HEALTH_ENDPOINT_NAME], ], ]; @@ -62,9 +62,9 @@ class ConfigProviderTest extends TestCase ['path' => '/baz/foo'], ], [ - ['path' => '/rest/v{version:1|2}/foo'], - ['path' => '/rest/v{version:1|2}/bar'], - ['path' => '/rest/v{version:1|2}/baz/foo'], + ['path' => '/rest/v{version:1|2|3}/foo'], + ['path' => '/rest/v{version:1|2|3}/bar'], + ['path' => '/rest/v{version:1|2|3}/baz/foo'], ], ]; } diff --git a/module/Rest/test/Exception/MissingAuthenticationExceptionTest.php b/module/Rest/test/Exception/MissingAuthenticationExceptionTest.php index 5d80ca17..ab79ba2f 100644 --- a/module/Rest/test/Exception/MissingAuthenticationExceptionTest.php +++ b/module/Rest/test/Exception/MissingAuthenticationExceptionTest.php @@ -65,7 +65,7 @@ class MissingAuthenticationExceptionTest extends TestCase private function assertCommonExceptionShape(MissingAuthenticationException $e): void { self::assertEquals('Invalid authorization', $e->getTitle()); - self::assertEquals('INVALID_AUTHORIZATION', $e->getType()); + self::assertEquals('https://shlink.io/api/error/missing-authentication', $e->getType()); self::assertEquals(401, $e->getStatus()); } } From 40bbcb325036887eae08a45e0d1682ef174a7c13 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 13 Aug 2022 17:48:55 +0200 Subject: [PATCH 031/108] Added some API tests for v3 API errors --- config/autoload/error-handler.global.php | 4 +- .../autoload/middleware-pipeline.global.php | 2 +- docs/swagger/parameters/version.json | 1 + docs/swagger/swagger.json | 2 +- .../test-api/Action/CreateShortUrlTest.php | 60 +++++++++++++++---- .../test-api/Action/DeleteShortUrlTest.php | 24 ++++++++ .../Rest/test-api/Action/DeleteTagsTest.php | 13 ++-- .../Rest/test-api/Action/DomainVisitsTest.php | 19 ++++++ 8 files changed, 105 insertions(+), 20 deletions(-) diff --git a/config/autoload/error-handler.global.php b/config/autoload/error-handler.global.php index b4872bfe..2cf28a47 100644 --- a/config/autoload/error-handler.global.php +++ b/config/autoload/error-handler.global.php @@ -10,8 +10,8 @@ return [ 'problem-details' => [ 'default_types_map' => [ - 404 => 'NOT_FOUND', - 500 => 'INTERNAL_SERVER_ERROR', + 404 => 'NOT_FOUND', // TODO Define new values, with backwards compatibility if possible + 500 => 'INTERNAL_SERVER_ERROR', // TODO Define new values, with backwards compatibility if possible ], ], diff --git a/config/autoload/middleware-pipeline.global.php b/config/autoload/middleware-pipeline.global.php index 5291db8c..25db6b7b 100644 --- a/config/autoload/middleware-pipeline.global.php +++ b/config/autoload/middleware-pipeline.global.php @@ -26,7 +26,6 @@ return [ 'path' => '/rest', 'middleware' => [ ProblemDetails\ProblemDetailsMiddleware::class, - Rest\Middleware\ErrorHandler\BackwardsCompatibleProblemDetailsHandler::class, ], ], @@ -46,6 +45,7 @@ return [ 'rest' => [ 'path' => '/rest', 'middleware' => [ + Rest\Middleware\ErrorHandler\BackwardsCompatibleProblemDetailsHandler::class, Router\Middleware\ImplicitOptionsMiddleware::class, Rest\Middleware\BodyParserMiddleware::class, Rest\Middleware\AuthenticationMiddleware::class, diff --git a/docs/swagger/parameters/version.json b/docs/swagger/parameters/version.json index c2b1cc1a..abb7e0f7 100644 --- a/docs/swagger/parameters/version.json +++ b/docs/swagger/parameters/version.json @@ -6,6 +6,7 @@ "schema": { "type": "string", "enum": [ + "3", "2", "1" ] diff --git a/docs/swagger/swagger.json b/docs/swagger/swagger.json index 840ac84e..b80ae3b2 100644 --- a/docs/swagger/swagger.json +++ b/docs/swagger/swagger.json @@ -3,7 +3,7 @@ "info": { "title": "Shlink", "description": "Shlink, the self-hosted URL shortener", - "version": "2.0" + "version": "3.0" }, "externalDocs": { diff --git a/module/Rest/test-api/Action/CreateShortUrlTest.php b/module/Rest/test-api/Action/CreateShortUrlTest.php index 2fe529a3..26d271f0 100644 --- a/module/Rest/test-api/Action/CreateShortUrlTest.php +++ b/module/Rest/test-api/Action/CreateShortUrlTest.php @@ -60,6 +60,25 @@ class CreateShortUrlTest extends ApiTestCase } } + /** + * @test + * @dataProvider provideDuplicatedSlugApiVersions + */ + public function expectedTypeIsReturnedForConflictingSlugBasedOnApiVersion( + string $version, + string $expectedType, + ): void { + [, $payload] = $this->createShortUrl(['customSlug' => 'custom'], version: $version); + self::assertEquals($expectedType, $payload['type']); + } + + public function provideDuplicatedSlugApiVersions(): iterable + { + yield ['1', 'INVALID_SLUG']; + yield ['2', 'INVALID_SLUG']; + yield ['3', 'https://shlink.io/api/error/non-unique-slug']; + } + /** * @test * @dataProvider provideTags @@ -226,15 +245,15 @@ class CreateShortUrlTest extends ApiTestCase * @test * @dataProvider provideInvalidUrls */ - public function failsToCreateShortUrlWithInvalidLongUrl(string $url): void + public function failsToCreateShortUrlWithInvalidLongUrl(string $url, string $version, string $expectedType): void { $expectedDetail = sprintf('Provided URL %s is invalid. Try with a different one.', $url); - [$statusCode, $payload] = $this->createShortUrl(['longUrl' => $url, 'validateUrl' => true]); + [$statusCode, $payload] = $this->createShortUrl(['longUrl' => $url, 'validateUrl' => true], version: $version); self::assertEquals(self::STATUS_BAD_REQUEST, $statusCode); self::assertEquals(self::STATUS_BAD_REQUEST, $payload['status']); - self::assertEquals('INVALID_URL', $payload['type']); + self::assertEquals($expectedType, $payload['type']); self::assertEquals($expectedDetail, $payload['detail']); self::assertEquals('Invalid URL', $payload['title']); self::assertEquals($url, $payload['url']); @@ -242,23 +261,37 @@ class CreateShortUrlTest extends ApiTestCase public function provideInvalidUrls(): iterable { - yield 'empty URL' => ['']; - yield 'non-reachable URL' => ['https://this-has-to-be-invalid.com']; + yield 'empty URL' => ['', '2', 'INVALID_URL']; + yield 'non-reachable URL' => ['https://this-has-to-be-invalid.com', '2', 'INVALID_URL']; + yield 'API version 3' => ['', '3', 'https://shlink.io/api/error/invalid-url']; } - /** @test */ - public function failsToCreateShortUrlWithoutLongUrl(): void + /** + * @test + * @dataProvider provideInvalidArgumentApiVersions + */ + public function failsToCreateShortUrlWithoutLongUrl(string $version, string $expectedType): void { - $resp = $this->callApiWithKey(self::METHOD_POST, '/short-urls', [RequestOptions::JSON => []]); + $resp = $this->callApiWithKey( + self::METHOD_POST, + sprintf('/rest/v%s/short-urls', $version), + [RequestOptions::JSON => []], + ); $payload = $this->getJsonResponsePayload($resp); self::assertEquals(self::STATUS_BAD_REQUEST, $resp->getStatusCode()); self::assertEquals(self::STATUS_BAD_REQUEST, $payload['status']); - self::assertEquals('INVALID_ARGUMENT', $payload['type']); + self::assertEquals($expectedType, $payload['type']); self::assertEquals('Provided data is not valid', $payload['detail']); self::assertEquals('Invalid data', $payload['title']); } + public function provideInvalidArgumentApiVersions(): iterable + { + yield ['2', 'INVALID_ARGUMENT']; + yield ['3', 'https://shlink.io/api/error/invalid-data']; + } + /** @test */ public function defaultDomainIsDroppedIfProvided(): void { @@ -332,12 +365,17 @@ class CreateShortUrlTest extends ApiTestCase /** * @return array{int $statusCode, array $payload} */ - private function createShortUrl(array $body = [], string $apiKey = 'valid_api_key'): array + private function createShortUrl(array $body = [], string $apiKey = 'valid_api_key', string $version = '2'): array { if (! isset($body['longUrl'])) { $body['longUrl'] = 'https://app.shlink.io'; } - $resp = $this->callApiWithKey(self::METHOD_POST, '/short-urls', [RequestOptions::JSON => $body], $apiKey); + $resp = $this->callApiWithKey( + self::METHOD_POST, + sprintf('/rest/v%s/short-urls', $version), + [RequestOptions::JSON => $body], + $apiKey, + ); $payload = $this->getJsonResponsePayload($resp); return [$resp->getStatusCode(), $payload]; diff --git a/module/Rest/test-api/Action/DeleteShortUrlTest.php b/module/Rest/test-api/Action/DeleteShortUrlTest.php index 5cac3dbd..f8ba6ef1 100644 --- a/module/Rest/test-api/Action/DeleteShortUrlTest.php +++ b/module/Rest/test-api/Action/DeleteShortUrlTest.php @@ -7,6 +7,8 @@ namespace ShlinkioApiTest\Shlink\Rest\Action; use Shlinkio\Shlink\TestUtils\ApiTest\ApiTestCase; use ShlinkioApiTest\Shlink\Rest\Utils\NotFoundUrlHelpersTrait; +use function sprintf; + class DeleteShortUrlTest extends ApiTestCase { use NotFoundUrlHelpersTrait; @@ -33,6 +35,28 @@ class DeleteShortUrlTest extends ApiTestCase self::assertEquals($domain, $payload['domain'] ?? null); } + /** + * @test + * @dataProvider provideApiVersions + */ + public function expectedTypeIsReturnedBasedOnApiVersion(string $version, string $expectedType): void + { + $resp = $this->callApiWithKey( + self::METHOD_DELETE, + sprintf('/rest/v%s/short-urls/invalid-short-code', $version), + ); + $payload = $this->getJsonResponsePayload($resp); + + self::assertEquals($expectedType, $payload['type']); + } + + public function provideApiVersions(): iterable + { + yield ['1', 'INVALID_SHORTCODE']; + yield ['2', 'INVALID_SHORTCODE']; + yield ['3', 'https://shlink.io/api/error/short-url-not-found']; + } + /** @test */ public function properShortUrlIsDeletedWhenDomainIsProvided(): void { diff --git a/module/Rest/test-api/Action/DeleteTagsTest.php b/module/Rest/test-api/Action/DeleteTagsTest.php index ca175b69..c81d7906 100644 --- a/module/Rest/test-api/Action/DeleteTagsTest.php +++ b/module/Rest/test-api/Action/DeleteTagsTest.php @@ -7,29 +7,32 @@ namespace ShlinkioApiTest\Shlink\Rest\Action; use GuzzleHttp\RequestOptions; use Shlinkio\Shlink\TestUtils\ApiTest\ApiTestCase; +use function sprintf; + class DeleteTagsTest extends ApiTestCase { /** * @test * @dataProvider provideNonAdminApiKeys */ - public function anErrorIsReturnedWithNonAdminApiKeys(string $apiKey): void + public function anErrorIsReturnedWithNonAdminApiKeys(string $apiKey, string $version, string $expectedType): void { - $resp = $this->callApiWithKey(self::METHOD_DELETE, '/tags', [ + $resp = $this->callApiWithKey(self::METHOD_DELETE, sprintf('/rest/v%s/tags', $version), [ RequestOptions::QUERY => ['tags' => ['foo']], ], $apiKey); $payload = $this->getJsonResponsePayload($resp); self::assertEquals(self::STATUS_FORBIDDEN, $resp->getStatusCode()); self::assertEquals(self::STATUS_FORBIDDEN, $payload['status']); - self::assertEquals('FORBIDDEN_OPERATION', $payload['type']); + self::assertEquals($expectedType, $payload['type']); self::assertEquals('You are not allowed to delete tags', $payload['detail']); self::assertEquals('Forbidden tag operation', $payload['title']); } public function provideNonAdminApiKeys(): iterable { - yield 'author' => ['author_api_key']; - yield 'domain' => ['domain_api_key']; + yield 'author' => ['author_api_key', '2', 'FORBIDDEN_OPERATION']; + yield 'domain' => ['domain_api_key', '2', 'FORBIDDEN_OPERATION']; + yield 'version 3' => ['domain_api_key', '3', 'https://shlink.io/api/error/forbidden-tag-operation']; } } diff --git a/module/Rest/test-api/Action/DomainVisitsTest.php b/module/Rest/test-api/Action/DomainVisitsTest.php index b6e29a12..c6c31ebb 100644 --- a/module/Rest/test-api/Action/DomainVisitsTest.php +++ b/module/Rest/test-api/Action/DomainVisitsTest.php @@ -65,4 +65,23 @@ class DomainVisitsTest extends ApiTestCase yield 'domain API key with not-owned valid domain' => ['domain_api_key', 'this_domain_is_detached.com']; yield 'author API key with valid domain not used in URLs' => ['author_api_key', 'this_domain_is_detached.com']; } + + /** + * @test + * @dataProvider provideApiVersions + */ + public function expectedNotFoundTypeIsReturnedForApiVersion(string $version, string $expectedType): void + { + $resp = $this->callApiWithKey(self::METHOD_GET, sprintf('/rest/v%s/domains/invalid.com/visits', $version)); + $payload = $this->getJsonResponsePayload($resp); + + self::assertEquals($expectedType, $payload['type']); + } + + public function provideApiVersions(): iterable + { + yield ['1', 'DOMAIN_NOT_FOUND']; + yield ['2', 'DOMAIN_NOT_FOUND']; + yield ['3', 'https://shlink.io/api/error/domain-not-found']; + } } From ce4bf62d75199203c4a98079732d98ea382f77bb Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 14 Aug 2022 10:34:27 +0200 Subject: [PATCH 032/108] Added more granular resolution of arguments for infection based on branch --- .github/workflows/ci-mutation-tests.yml | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci-mutation-tests.yml b/.github/workflows/ci-mutation-tests.yml index a4ce7432..f152ed15 100644 --- a/.github/workflows/ci-mutation-tests.yml +++ b/.github/workflows/ci-mutation-tests.yml @@ -24,9 +24,19 @@ jobs: - uses: actions/download-artifact@v3 with: path: build + - name: Resolve infection args + id: infection_args + run: | + BRANCH="${GITHUB_REF#refs/heads/}" | + if [[ $BRANCH == 'main' || $BRANCH == 'develop' ]]; then + echo "::set-output name=args::--logger-github=false" + else + echo "::set-output name=args::--logger-github=false --git-diff-lines --git-diff-base=develop" + fi; + shell: bash - if: ${{ inputs.test-group == 'unit' }} - run: composer infect:ci:unit -- --git-diff-lines --logger-github=false + run: composer infect:ci:unit -- ${{ steps.infection_args.outputs.args }} env: INFECTION_BADGE_API_KEY: ${{ secrets.INFECTION_BADGE_API_KEY }} - if: ${{ inputs.test-group != 'unit' }} - run: composer infect:ci:${{ inputs.test-group }} -- --git-diff-lines --logger-github=false + run: composer infect:ci:${{ inputs.test-group }} -- ${{ steps.infection_args.outputs.args }} From 4a122e0209d95deaf6f3f90fbb415dd40c6473cd Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 14 Aug 2022 10:51:12 +0200 Subject: [PATCH 033/108] Added remaining API tests covering error type convertions --- module/Rest/test-api/Action/UpdateTagTest.php | 38 +++++++++++++++---- .../Middleware/AuthenticationTest.php | 38 +++++++++++++------ 2 files changed, 57 insertions(+), 19 deletions(-) diff --git a/module/Rest/test-api/Action/UpdateTagTest.php b/module/Rest/test-api/Action/UpdateTagTest.php index 262789d7..414e7670 100644 --- a/module/Rest/test-api/Action/UpdateTagTest.php +++ b/module/Rest/test-api/Action/UpdateTagTest.php @@ -7,6 +7,8 @@ namespace ShlinkioApiTest\Shlink\Rest\Action; use GuzzleHttp\RequestOptions; use Shlinkio\Shlink\TestUtils\ApiTest\ApiTestCase; +use function sprintf; + class UpdateTagTest extends ApiTestCase { /** @@ -34,12 +36,15 @@ class UpdateTagTest extends ApiTestCase yield [['newName' => 'foo']]; } - /** @test */ - public function tryingToRenameInvalidTagReturnsNotFound(): void + /** + * @test + * @dataProvider provideTagNotFoundApiVersions + */ + public function tryingToRenameInvalidTagReturnsNotFound(string $version, string $expectedType): void { $expectedDetail = 'Tag with name "invalid_tag" could not be found'; - $resp = $this->callApiWithKey(self::METHOD_PUT, '/tags', [RequestOptions::JSON => [ + $resp = $this->callApiWithKey(self::METHOD_PUT, sprintf('/rest/v%s/tags', $version), [RequestOptions::JSON => [ 'oldName' => 'invalid_tag', 'newName' => 'foo', ]]); @@ -47,17 +52,27 @@ class UpdateTagTest extends ApiTestCase self::assertEquals(self::STATUS_NOT_FOUND, $resp->getStatusCode()); self::assertEquals(self::STATUS_NOT_FOUND, $payload['status']); - self::assertEquals('TAG_NOT_FOUND', $payload['type']); + self::assertEquals($expectedType, $payload['type']); self::assertEquals($expectedDetail, $payload['detail']); self::assertEquals('Tag not found', $payload['title']); } - /** @test */ - public function errorIsThrownWhenTryingToRenameTagToAnotherTagName(): void + public function provideTagNotFoundApiVersions(): iterable + { + yield 'version 1' => ['1', 'TAG_NOT_FOUND']; + yield 'version 2' => ['2', 'TAG_NOT_FOUND']; + yield 'version 3' => ['3', 'https://shlink.io/api/error/tag-not-found']; + } + + /** + * @test + * @dataProvider provideTagConflictsApiVersions + */ + public function errorIsThrownWhenTryingToRenameTagToAnotherTagName(string $version, string $expectedType): void { $expectedDetail = 'You cannot rename tag foo to bar, because it already exists'; - $resp = $this->callApiWithKey(self::METHOD_PUT, '/tags', [RequestOptions::JSON => [ + $resp = $this->callApiWithKey(self::METHOD_PUT, sprintf('/rest/v%s/tags', $version), [RequestOptions::JSON => [ 'oldName' => 'foo', 'newName' => 'bar', ]]); @@ -65,11 +80,18 @@ class UpdateTagTest extends ApiTestCase self::assertEquals(self::STATUS_CONFLICT, $resp->getStatusCode()); self::assertEquals(self::STATUS_CONFLICT, $payload['status']); - self::assertEquals('TAG_CONFLICT', $payload['type']); + self::assertEquals($expectedType, $payload['type']); self::assertEquals($expectedDetail, $payload['detail']); self::assertEquals('Tag conflict', $payload['title']); } + public function provideTagConflictsApiVersions(): iterable + { + yield 'version 1' => ['1', 'TAG_CONFLICT']; + yield 'version 2' => ['2', 'TAG_CONFLICT']; + yield 'version 3' => ['3', 'https://shlink.io/api/error/tag-conflict']; + } + /** @test */ public function tagIsProperlyRenamedWhenRenamingToItself(): void { diff --git a/module/Rest/test-api/Middleware/AuthenticationTest.php b/module/Rest/test-api/Middleware/AuthenticationTest.php index 61dbd2c5..51128079 100644 --- a/module/Rest/test-api/Middleware/AuthenticationTest.php +++ b/module/Rest/test-api/Middleware/AuthenticationTest.php @@ -6,32 +6,47 @@ namespace ShlinkioApiTest\Shlink\Rest\Middleware; use Shlinkio\Shlink\TestUtils\ApiTest\ApiTestCase; +use function sprintf; + class AuthenticationTest extends ApiTestCase { - /** @test */ - public function authorizationErrorIsReturnedIfNoApiKeyIsSent(): void + /** + * @test + * @dataProvider provideApiVersions + */ + public function authorizationErrorIsReturnedIfNoApiKeyIsSent(string $version, string $expectedType): void { $expectedDetail = 'Expected one of the following authentication headers, ["X-Api-Key"], but none were provided'; - $resp = $this->callApi(self::METHOD_GET, '/short-urls'); + $resp = $this->callApi(self::METHOD_GET, sprintf('/rest/v%s/short-urls', $version)); $payload = $this->getJsonResponsePayload($resp); self::assertEquals(self::STATUS_UNAUTHORIZED, $resp->getStatusCode()); self::assertEquals(self::STATUS_UNAUTHORIZED, $payload['status']); - self::assertEquals('INVALID_AUTHORIZATION', $payload['type']); + self::assertEquals($expectedType, $payload['type']); self::assertEquals($expectedDetail, $payload['detail']); self::assertEquals('Invalid authorization', $payload['title']); } + public function provideApiVersions(): iterable + { + yield 'version 1' => ['1', 'INVALID_AUTHORIZATION']; + yield 'version 2' => ['2', 'INVALID_AUTHORIZATION']; + yield 'version 3' => ['3', 'https://shlink.io/api/error/missing-authentication']; + } + /** * @test * @dataProvider provideInvalidApiKeys */ - public function apiKeyErrorIsReturnedWhenProvidedApiKeyIsInvalid(string $apiKey): void - { + public function apiKeyErrorIsReturnedWhenProvidedApiKeyIsInvalid( + string $apiKey, + string $version, + string $expectedType, + ): void { $expectedDetail = 'Provided API key does not exist or is invalid.'; - $resp = $this->callApi(self::METHOD_GET, '/short-urls', [ + $resp = $this->callApi(self::METHOD_GET, sprintf('/rest/v%s/short-urls', $version), [ 'headers' => [ 'X-Api-Key' => $apiKey, ], @@ -40,15 +55,16 @@ class AuthenticationTest extends ApiTestCase self::assertEquals(self::STATUS_UNAUTHORIZED, $resp->getStatusCode()); self::assertEquals(self::STATUS_UNAUTHORIZED, $payload['status']); - self::assertEquals('INVALID_API_KEY', $payload['type']); + self::assertEquals($expectedType, $payload['type']); self::assertEquals($expectedDetail, $payload['detail']); self::assertEquals('Invalid API key', $payload['title']); } public function provideInvalidApiKeys(): iterable { - yield 'key which does not exist' => ['invalid']; - yield 'key which is expired' => ['expired_api_key']; - yield 'key which is disabled' => ['disabled_api_key']; + yield 'key which does not exist' => ['invalid', '2', 'INVALID_API_KEY']; + yield 'key which is expired' => ['expired_api_key', '2', 'INVALID_API_KEY']; + yield 'key which is disabled' => ['disabled_api_key', '2', 'INVALID_API_KEY']; + yield 'version 3' => ['disabled_api_key', '3', 'https://shlink.io/api/error/invalid-api-key']; } } From 2650cb89b5eb03a1dfa855fff9aafd593e127706 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 14 Aug 2022 12:39:05 +0200 Subject: [PATCH 034/108] Created BackwardsCompatibleProblemDetailsExceptionTest --- ...sCompatibleProblemDetailsExceptionTest.php | 114 ++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 module/Rest/test/Exception/BackwardsCompatibleProblemDetailsExceptionTest.php diff --git a/module/Rest/test/Exception/BackwardsCompatibleProblemDetailsExceptionTest.php b/module/Rest/test/Exception/BackwardsCompatibleProblemDetailsExceptionTest.php new file mode 100644 index 00000000..13df168b --- /dev/null +++ b/module/Rest/test/Exception/BackwardsCompatibleProblemDetailsExceptionTest.php @@ -0,0 +1,114 @@ +type; + } + + public function getTitle(): string + { + return 'title'; + } + + public function getDetail(): string + { + return 'detail'; + } + + public function getAdditionalData(): array + { + return []; + } + + public function toArray(): array + { + return ['type' => $this->type]; + } + + public function jsonSerialize(): array + { + return ['type' => $this->type]; + } + }; + $e = BackwardsCompatibleProblemDetailsException::fromProblemDetails($original); + + self::assertEquals($e->getType(), $expectedType); + self::assertEquals($e->toArray(), ['type' => $expectedType]); + self::assertEquals($e->jsonSerialize(), ['type' => $expectedType]); + + self::assertEquals($original->getTitle(), $e->getTitle()); + self::assertEquals($original->getDetail(), $e->getDetail()); + self::assertEquals($original->getAdditionalData(), $e->getAdditionalData()); + + if ($expectSameType) { + self::assertEquals($original->getType(), $e->getType()); + self::assertEquals($original->toArray(), $e->toArray()); + self::assertEquals($original->jsonSerialize(), $e->jsonSerialize()); + } else { + self::assertNotEquals($original->getType(), $e->getType()); + self::assertNotEquals($original->toArray(), $e->toArray()); + self::assertNotEquals($original->jsonSerialize(), $e->jsonSerialize()); + } + } + + public function provideTypes(): iterable + { + yield ['foo', 'foo', true]; + yield ['bar', 'bar', true]; + yield [ValidationException::TYPE, 'INVALID_ARGUMENT']; + yield [DeleteShortUrlException::TYPE, 'INVALID_SHORT_URL_DELETION']; + yield [DomainNotFoundException::TYPE, 'DOMAIN_NOT_FOUND']; + yield [ForbiddenTagOperationException::TYPE, 'FORBIDDEN_OPERATION']; + yield [InvalidUrlException::TYPE, 'INVALID_URL']; + yield [NonUniqueSlugException::TYPE, 'INVALID_SLUG']; + yield [ShortUrlNotFoundException::TYPE, 'INVALID_SHORTCODE']; + yield [TagConflictException::TYPE, 'TAG_CONFLICT']; + yield [TagNotFoundException::TYPE, 'TAG_NOT_FOUND']; + yield [MercureException::TYPE, 'MERCURE_NOT_CONFIGURED']; + yield [MissingAuthenticationException::TYPE, 'INVALID_AUTHORIZATION']; + yield [VerifyAuthenticationException::TYPE, 'INVALID_API_KEY']; + } +} From a41835573b959357724c9d43558f5ee47ca82e1c Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 14 Aug 2022 13:12:10 +0200 Subject: [PATCH 035/108] Centralized prefix for problem detail types --- config/autoload/error-handler.global.php | 6 ++-- module/Core/functions/functions.php | 5 ++++ .../src/Exception/DeleteShortUrlException.php | 5 ++-- .../src/Exception/DomainNotFoundException.php | 5 ++-- .../ForbiddenTagOperationException.php | 6 ++-- .../src/Exception/InvalidUrlException.php | 5 ++-- .../src/Exception/NonUniqueSlugException.php | 5 ++-- .../Exception/ShortUrlNotFoundException.php | 5 ++-- .../src/Exception/TagConflictException.php | 5 ++-- .../src/Exception/TagNotFoundException.php | 5 ++-- .../src/Exception/ValidationException.php | 5 ++-- ...wardsCompatibleProblemDetailsException.php | 30 +++++++++++-------- .../Rest/src/Exception/MercureException.php | 6 ++-- .../MissingAuthenticationException.php | 5 ++-- .../VerifyAuthenticationException.php | 6 ++-- ...sCompatibleProblemDetailsExceptionTest.php | 24 +++++++-------- 16 files changed, 77 insertions(+), 51 deletions(-) diff --git a/config/autoload/error-handler.global.php b/config/autoload/error-handler.global.php index 2cf28a47..65e5b616 100644 --- a/config/autoload/error-handler.global.php +++ b/config/autoload/error-handler.global.php @@ -6,12 +6,14 @@ use Laminas\Stratigility\Middleware\ErrorHandler; use Mezzio\ProblemDetails\ProblemDetailsMiddleware; use Shlinkio\Shlink\Common\Logger; +use function Shlinkio\Shlink\Core\toProblemDetailsType; + return [ 'problem-details' => [ 'default_types_map' => [ - 404 => 'NOT_FOUND', // TODO Define new values, with backwards compatibility if possible - 500 => 'INTERNAL_SERVER_ERROR', // TODO Define new values, with backwards compatibility if possible + 404 => toProblemDetailsType('not-found'), + 500 => toProblemDetailsType('internal-server-error'), ], ], diff --git a/module/Core/functions/functions.php b/module/Core/functions/functions.php index c5186e41..d34175c7 100644 --- a/module/Core/functions/functions.php +++ b/module/Core/functions/functions.php @@ -127,3 +127,8 @@ function camelCaseToHumanFriendly(string $value): string return ucfirst($filter->filter($value)); } + +function toProblemDetailsType(string $errorCode): string +{ + return sprintf('https://shlink.io/api/error/%s', $errorCode); +} diff --git a/module/Core/src/Exception/DeleteShortUrlException.php b/module/Core/src/Exception/DeleteShortUrlException.php index f6638221..f8a5cfa8 100644 --- a/module/Core/src/Exception/DeleteShortUrlException.php +++ b/module/Core/src/Exception/DeleteShortUrlException.php @@ -9,6 +9,7 @@ use Mezzio\ProblemDetails\Exception\CommonProblemDetailsExceptionTrait; use Mezzio\ProblemDetails\Exception\ProblemDetailsExceptionInterface; use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier; +use function Shlinkio\Shlink\Core\toProblemDetailsType; use function sprintf; class DeleteShortUrlException extends DomainException implements ProblemDetailsExceptionInterface @@ -16,7 +17,7 @@ class DeleteShortUrlException extends DomainException implements ProblemDetailsE use CommonProblemDetailsExceptionTrait; private const TITLE = 'Cannot delete short URL'; - public const TYPE = 'https://shlink.io/api/error/invalid-short-url-deletion'; + public const ERROR_CODE = 'invalid-short-url-deletion'; public static function fromVisitsThreshold(int $threshold, ShortUrlIdentifier $identifier): self { @@ -32,7 +33,7 @@ class DeleteShortUrlException extends DomainException implements ProblemDetailsE $e->detail = $e->getMessage(); $e->title = self::TITLE; - $e->type = self::TYPE; + $e->type = toProblemDetailsType(self::ERROR_CODE); $e->status = StatusCodeInterface::STATUS_UNPROCESSABLE_ENTITY; $e->additional = [ 'shortCode' => $shortCode, diff --git a/module/Core/src/Exception/DomainNotFoundException.php b/module/Core/src/Exception/DomainNotFoundException.php index aca67813..688a4edc 100644 --- a/module/Core/src/Exception/DomainNotFoundException.php +++ b/module/Core/src/Exception/DomainNotFoundException.php @@ -8,6 +8,7 @@ use Fig\Http\Message\StatusCodeInterface; use Mezzio\ProblemDetails\Exception\CommonProblemDetailsExceptionTrait; use Mezzio\ProblemDetails\Exception\ProblemDetailsExceptionInterface; +use function Shlinkio\Shlink\Core\toProblemDetailsType; use function sprintf; class DomainNotFoundException extends DomainException implements ProblemDetailsExceptionInterface @@ -15,7 +16,7 @@ class DomainNotFoundException extends DomainException implements ProblemDetailsE use CommonProblemDetailsExceptionTrait; private const TITLE = 'Domain not found'; - public const TYPE = 'https://shlink.io/api/error/domain-not-found'; + public const ERROR_CODE = 'domain-not-found'; private function __construct(string $message, array $additional) { @@ -23,7 +24,7 @@ class DomainNotFoundException extends DomainException implements ProblemDetailsE $this->detail = $message; $this->title = self::TITLE; - $this->type = self::TYPE; + $this->type = toProblemDetailsType(self::ERROR_CODE); $this->status = StatusCodeInterface::STATUS_NOT_FOUND; $this->additional = $additional; } diff --git a/module/Core/src/Exception/ForbiddenTagOperationException.php b/module/Core/src/Exception/ForbiddenTagOperationException.php index 6da1cb61..64ae156c 100644 --- a/module/Core/src/Exception/ForbiddenTagOperationException.php +++ b/module/Core/src/Exception/ForbiddenTagOperationException.php @@ -8,12 +8,14 @@ use Fig\Http\Message\StatusCodeInterface; use Mezzio\ProblemDetails\Exception\CommonProblemDetailsExceptionTrait; use Mezzio\ProblemDetails\Exception\ProblemDetailsExceptionInterface; +use function Shlinkio\Shlink\Core\toProblemDetailsType; + class ForbiddenTagOperationException extends DomainException implements ProblemDetailsExceptionInterface { use CommonProblemDetailsExceptionTrait; private const TITLE = 'Forbidden tag operation'; - public const TYPE = 'https://shlink.io/api/error/forbidden-tag-operation'; + public const ERROR_CODE = 'forbidden-tag-operation'; public static function forDeletion(): self { @@ -31,7 +33,7 @@ class ForbiddenTagOperationException extends DomainException implements ProblemD $e->detail = $message; $e->title = self::TITLE; - $e->type = self::TYPE; + $e->type = toProblemDetailsType(self::ERROR_CODE); $e->status = StatusCodeInterface::STATUS_FORBIDDEN; return $e; diff --git a/module/Core/src/Exception/InvalidUrlException.php b/module/Core/src/Exception/InvalidUrlException.php index 6d1f93c6..200914c2 100644 --- a/module/Core/src/Exception/InvalidUrlException.php +++ b/module/Core/src/Exception/InvalidUrlException.php @@ -9,6 +9,7 @@ use Mezzio\ProblemDetails\Exception\CommonProblemDetailsExceptionTrait; use Mezzio\ProblemDetails\Exception\ProblemDetailsExceptionInterface; use Throwable; +use function Shlinkio\Shlink\Core\toProblemDetailsType; use function sprintf; class InvalidUrlException extends DomainException implements ProblemDetailsExceptionInterface @@ -16,7 +17,7 @@ class InvalidUrlException extends DomainException implements ProblemDetailsExcep use CommonProblemDetailsExceptionTrait; private const TITLE = 'Invalid URL'; - public const TYPE = 'https://shlink.io/api/error/invalid-url'; + public const ERROR_CODE = 'invalid-url'; public static function fromUrl(string $url, ?Throwable $previous = null): self { @@ -25,7 +26,7 @@ class InvalidUrlException extends DomainException implements ProblemDetailsExcep $e->detail = $e->getMessage(); $e->title = self::TITLE; - $e->type = self::TYPE; + $e->type = toProblemDetailsType(self::ERROR_CODE); $e->status = $status; $e->additional = ['url' => $url]; diff --git a/module/Core/src/Exception/NonUniqueSlugException.php b/module/Core/src/Exception/NonUniqueSlugException.php index dc1fcca9..5336786c 100644 --- a/module/Core/src/Exception/NonUniqueSlugException.php +++ b/module/Core/src/Exception/NonUniqueSlugException.php @@ -9,6 +9,7 @@ use Mezzio\ProblemDetails\Exception\CommonProblemDetailsExceptionTrait; use Mezzio\ProblemDetails\Exception\ProblemDetailsExceptionInterface; use Shlinkio\Shlink\Importer\Model\ImportedShlinkUrl; +use function Shlinkio\Shlink\Core\toProblemDetailsType; use function sprintf; class NonUniqueSlugException extends InvalidArgumentException implements ProblemDetailsExceptionInterface @@ -16,7 +17,7 @@ class NonUniqueSlugException extends InvalidArgumentException implements Problem use CommonProblemDetailsExceptionTrait; private const TITLE = 'Invalid custom slug'; - public const TYPE = 'https://shlink.io/api/error/non-unique-slug'; + public const ERROR_CODE = 'non-unique-slug'; public static function fromSlug(string $slug, ?string $domain = null): self { @@ -25,7 +26,7 @@ class NonUniqueSlugException extends InvalidArgumentException implements Problem $e->detail = $e->getMessage(); $e->title = self::TITLE; - $e->type = self::TYPE; + $e->type = toProblemDetailsType(self::ERROR_CODE); $e->status = StatusCodeInterface::STATUS_BAD_REQUEST; $e->additional = ['customSlug' => $slug]; diff --git a/module/Core/src/Exception/ShortUrlNotFoundException.php b/module/Core/src/Exception/ShortUrlNotFoundException.php index 4da6972e..49b8cc02 100644 --- a/module/Core/src/Exception/ShortUrlNotFoundException.php +++ b/module/Core/src/Exception/ShortUrlNotFoundException.php @@ -9,6 +9,7 @@ use Mezzio\ProblemDetails\Exception\CommonProblemDetailsExceptionTrait; use Mezzio\ProblemDetails\Exception\ProblemDetailsExceptionInterface; use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier; +use function Shlinkio\Shlink\Core\toProblemDetailsType; use function sprintf; class ShortUrlNotFoundException extends DomainException implements ProblemDetailsExceptionInterface @@ -16,7 +17,7 @@ class ShortUrlNotFoundException extends DomainException implements ProblemDetail use CommonProblemDetailsExceptionTrait; private const TITLE = 'Short URL not found'; - public const TYPE = 'https://shlink.io/api/error/short-url-not-found'; + public const ERROR_CODE = 'short-url-not-found'; public static function fromNotFound(ShortUrlIdentifier $identifier): self { @@ -27,7 +28,7 @@ class ShortUrlNotFoundException extends DomainException implements ProblemDetail $e->detail = $e->getMessage(); $e->title = self::TITLE; - $e->type = self::TYPE; + $e->type = toProblemDetailsType(self::ERROR_CODE); $e->status = StatusCodeInterface::STATUS_NOT_FOUND; $e->additional = ['shortCode' => $shortCode]; diff --git a/module/Core/src/Exception/TagConflictException.php b/module/Core/src/Exception/TagConflictException.php index 09ea7be4..0fc5c317 100644 --- a/module/Core/src/Exception/TagConflictException.php +++ b/module/Core/src/Exception/TagConflictException.php @@ -9,6 +9,7 @@ use Mezzio\ProblemDetails\Exception\CommonProblemDetailsExceptionTrait; use Mezzio\ProblemDetails\Exception\ProblemDetailsExceptionInterface; use Shlinkio\Shlink\Core\Tag\Model\TagRenaming; +use function Shlinkio\Shlink\Core\toProblemDetailsType; use function sprintf; class TagConflictException extends RuntimeException implements ProblemDetailsExceptionInterface @@ -16,7 +17,7 @@ class TagConflictException extends RuntimeException implements ProblemDetailsExc use CommonProblemDetailsExceptionTrait; private const TITLE = 'Tag conflict'; - public const TYPE = 'https://shlink.io/api/error/tag-conflict'; + public const ERROR_CODE = 'tag-conflict'; public static function forExistingTag(TagRenaming $renaming): self { @@ -24,7 +25,7 @@ class TagConflictException extends RuntimeException implements ProblemDetailsExc $e->detail = $e->getMessage(); $e->title = self::TITLE; - $e->type = self::TYPE; + $e->type = toProblemDetailsType(self::ERROR_CODE); $e->status = StatusCodeInterface::STATUS_CONFLICT; $e->additional = $renaming->toArray(); diff --git a/module/Core/src/Exception/TagNotFoundException.php b/module/Core/src/Exception/TagNotFoundException.php index da2426aa..8fdd395a 100644 --- a/module/Core/src/Exception/TagNotFoundException.php +++ b/module/Core/src/Exception/TagNotFoundException.php @@ -8,6 +8,7 @@ use Fig\Http\Message\StatusCodeInterface; use Mezzio\ProblemDetails\Exception\CommonProblemDetailsExceptionTrait; use Mezzio\ProblemDetails\Exception\ProblemDetailsExceptionInterface; +use function Shlinkio\Shlink\Core\toProblemDetailsType; use function sprintf; class TagNotFoundException extends DomainException implements ProblemDetailsExceptionInterface @@ -15,7 +16,7 @@ class TagNotFoundException extends DomainException implements ProblemDetailsExce use CommonProblemDetailsExceptionTrait; private const TITLE = 'Tag not found'; - public const TYPE = 'https://shlink.io/api/error/tag-not-found'; + public const ERROR_CODE = 'tag-not-found'; public static function fromTag(string $tag): self { @@ -23,7 +24,7 @@ class TagNotFoundException extends DomainException implements ProblemDetailsExce $e->detail = $e->getMessage(); $e->title = self::TITLE; - $e->type = self::TYPE; + $e->type = toProblemDetailsType(self::ERROR_CODE); $e->status = StatusCodeInterface::STATUS_NOT_FOUND; $e->additional = ['tag' => $tag]; diff --git a/module/Core/src/Exception/ValidationException.php b/module/Core/src/Exception/ValidationException.php index 0f625bc5..dcb11fa4 100644 --- a/module/Core/src/Exception/ValidationException.php +++ b/module/Core/src/Exception/ValidationException.php @@ -12,6 +12,7 @@ use Throwable; use function array_keys; use function Shlinkio\Shlink\Core\arrayToString; +use function Shlinkio\Shlink\Core\toProblemDetailsType; use function sprintf; use const PHP_EOL; @@ -21,7 +22,7 @@ class ValidationException extends InvalidArgumentException implements ProblemDet use CommonProblemDetailsExceptionTrait; private const TITLE = 'Invalid data'; - public const TYPE = 'https://shlink.io/api/error/invalid-data'; + public const ERROR_CODE = 'invalid-data'; private array $invalidElements; @@ -37,7 +38,7 @@ class ValidationException extends InvalidArgumentException implements ProblemDet $e->detail = $e->getMessage(); $e->title = self::TITLE; - $e->type = self::TYPE; + $e->type = toProblemDetailsType(self::ERROR_CODE); $e->status = StatusCodeInterface::STATUS_BAD_REQUEST; $e->invalidElements = $invalidData; $e->additional = ['invalidElements' => array_keys($invalidData)]; diff --git a/module/Rest/src/Exception/BackwardsCompatibleProblemDetailsException.php b/module/Rest/src/Exception/BackwardsCompatibleProblemDetailsException.php index 14a6e934..685d3795 100644 --- a/module/Rest/src/Exception/BackwardsCompatibleProblemDetailsException.php +++ b/module/Rest/src/Exception/BackwardsCompatibleProblemDetailsException.php @@ -15,6 +15,9 @@ use Shlinkio\Shlink\Core\Exception\TagConflictException; use Shlinkio\Shlink\Core\Exception\TagNotFoundException; use Shlinkio\Shlink\Core\Exception\ValidationException; +use function explode; +use function Functional\last; + /** @deprecated */ class BackwardsCompatibleProblemDetailsException extends RuntimeException implements ProblemDetailsExceptionInterface { @@ -74,19 +77,20 @@ class BackwardsCompatibleProblemDetailsException extends RuntimeException implem private function remapType(string $wrappedType): string { - return match ($wrappedType) { - ValidationException::TYPE => 'INVALID_ARGUMENT', - DeleteShortUrlException::TYPE => 'INVALID_SHORT_URL_DELETION', - DomainNotFoundException::TYPE => 'DOMAIN_NOT_FOUND', - ForbiddenTagOperationException::TYPE => 'FORBIDDEN_OPERATION', - InvalidUrlException::TYPE => 'INVALID_URL', - NonUniqueSlugException::TYPE => 'INVALID_SLUG', - ShortUrlNotFoundException::TYPE => 'INVALID_SHORTCODE', - TagConflictException::TYPE => 'TAG_CONFLICT', - TagNotFoundException::TYPE => 'TAG_NOT_FOUND', - MercureException::TYPE => 'MERCURE_NOT_CONFIGURED', - MissingAuthenticationException::TYPE => 'INVALID_AUTHORIZATION', - VerifyAuthenticationException::TYPE => 'INVALID_API_KEY', + $lastSegment = last(explode('/', $wrappedType)); + return match ($lastSegment) { + ValidationException::ERROR_CODE => 'INVALID_ARGUMENT', + DeleteShortUrlException::ERROR_CODE => 'INVALID_SHORT_URL_DELETION', + DomainNotFoundException::ERROR_CODE => 'DOMAIN_NOT_FOUND', + ForbiddenTagOperationException::ERROR_CODE => 'FORBIDDEN_OPERATION', + InvalidUrlException::ERROR_CODE => 'INVALID_URL', + NonUniqueSlugException::ERROR_CODE => 'INVALID_SLUG', + ShortUrlNotFoundException::ERROR_CODE => 'INVALID_SHORTCODE', + TagConflictException::ERROR_CODE => 'TAG_CONFLICT', + TagNotFoundException::ERROR_CODE => 'TAG_NOT_FOUND', + MercureException::ERROR_CODE => 'MERCURE_NOT_CONFIGURED', + MissingAuthenticationException::ERROR_CODE => 'INVALID_AUTHORIZATION', + VerifyAuthenticationException::ERROR_CODE => 'INVALID_API_KEY', default => $wrappedType, }; } diff --git a/module/Rest/src/Exception/MercureException.php b/module/Rest/src/Exception/MercureException.php index 0e9a6edb..7e47b519 100644 --- a/module/Rest/src/Exception/MercureException.php +++ b/module/Rest/src/Exception/MercureException.php @@ -8,12 +8,14 @@ use Fig\Http\Message\StatusCodeInterface; use Mezzio\ProblemDetails\Exception\CommonProblemDetailsExceptionTrait; use Mezzio\ProblemDetails\Exception\ProblemDetailsExceptionInterface; +use function Shlinkio\Shlink\Core\toProblemDetailsType; + class MercureException extends RuntimeException implements ProblemDetailsExceptionInterface { use CommonProblemDetailsExceptionTrait; private const TITLE = 'Mercure integration not configured'; - public const TYPE = 'https://shlink.io/api/error/mercure-not-configured'; + public const ERROR_CODE = 'mercure-not-configured'; public static function mercureNotConfigured(): self { @@ -21,7 +23,7 @@ class MercureException extends RuntimeException implements ProblemDetailsExcepti $e->detail = $e->getMessage(); $e->title = self::TITLE; - $e->type = self::TYPE; + $e->type = toProblemDetailsType(self::ERROR_CODE); $e->status = StatusCodeInterface::STATUS_NOT_IMPLEMENTED; return $e; diff --git a/module/Rest/src/Exception/MissingAuthenticationException.php b/module/Rest/src/Exception/MissingAuthenticationException.php index 3c1f5b9f..3fd2e2c6 100644 --- a/module/Rest/src/Exception/MissingAuthenticationException.php +++ b/module/Rest/src/Exception/MissingAuthenticationException.php @@ -9,6 +9,7 @@ use Mezzio\ProblemDetails\Exception\CommonProblemDetailsExceptionTrait; use Mezzio\ProblemDetails\Exception\ProblemDetailsExceptionInterface; use function implode; +use function Shlinkio\Shlink\Core\toProblemDetailsType; use function sprintf; class MissingAuthenticationException extends RuntimeException implements ProblemDetailsExceptionInterface @@ -16,7 +17,7 @@ class MissingAuthenticationException extends RuntimeException implements Problem use CommonProblemDetailsExceptionTrait; private const TITLE = 'Invalid authorization'; - public const TYPE = 'https://shlink.io/api/error/missing-authentication'; + public const ERROR_CODE = 'missing-authentication'; public static function forHeaders(array $expectedHeaders): self { @@ -43,7 +44,7 @@ class MissingAuthenticationException extends RuntimeException implements Problem $e->detail = $message; $e->title = self::TITLE; - $e->type = self::TYPE; + $e->type = toProblemDetailsType(self::ERROR_CODE); $e->status = StatusCodeInterface::STATUS_UNAUTHORIZED; return $e; diff --git a/module/Rest/src/Exception/VerifyAuthenticationException.php b/module/Rest/src/Exception/VerifyAuthenticationException.php index 25541d03..25f1b050 100644 --- a/module/Rest/src/Exception/VerifyAuthenticationException.php +++ b/module/Rest/src/Exception/VerifyAuthenticationException.php @@ -8,11 +8,13 @@ use Fig\Http\Message\StatusCodeInterface; use Mezzio\ProblemDetails\Exception\CommonProblemDetailsExceptionTrait; use Mezzio\ProblemDetails\Exception\ProblemDetailsExceptionInterface; +use function Shlinkio\Shlink\Core\toProblemDetailsType; + class VerifyAuthenticationException extends RuntimeException implements ProblemDetailsExceptionInterface { use CommonProblemDetailsExceptionTrait; - public const TYPE = 'https://shlink.io/api/error/invalid-api-key'; + public const ERROR_CODE = 'invalid-api-key'; public static function forInvalidApiKey(): self { @@ -20,7 +22,7 @@ class VerifyAuthenticationException extends RuntimeException implements ProblemD $e->detail = $e->getMessage(); $e->title = 'Invalid API key'; - $e->type = self::TYPE; + $e->type = toProblemDetailsType(self::ERROR_CODE); $e->status = StatusCodeInterface::STATUS_UNAUTHORIZED; return $e; diff --git a/module/Rest/test/Exception/BackwardsCompatibleProblemDetailsExceptionTest.php b/module/Rest/test/Exception/BackwardsCompatibleProblemDetailsExceptionTest.php index 13df168b..c63cee71 100644 --- a/module/Rest/test/Exception/BackwardsCompatibleProblemDetailsExceptionTest.php +++ b/module/Rest/test/Exception/BackwardsCompatibleProblemDetailsExceptionTest.php @@ -98,17 +98,17 @@ class BackwardsCompatibleProblemDetailsExceptionTest extends TestCase { yield ['foo', 'foo', true]; yield ['bar', 'bar', true]; - yield [ValidationException::TYPE, 'INVALID_ARGUMENT']; - yield [DeleteShortUrlException::TYPE, 'INVALID_SHORT_URL_DELETION']; - yield [DomainNotFoundException::TYPE, 'DOMAIN_NOT_FOUND']; - yield [ForbiddenTagOperationException::TYPE, 'FORBIDDEN_OPERATION']; - yield [InvalidUrlException::TYPE, 'INVALID_URL']; - yield [NonUniqueSlugException::TYPE, 'INVALID_SLUG']; - yield [ShortUrlNotFoundException::TYPE, 'INVALID_SHORTCODE']; - yield [TagConflictException::TYPE, 'TAG_CONFLICT']; - yield [TagNotFoundException::TYPE, 'TAG_NOT_FOUND']; - yield [MercureException::TYPE, 'MERCURE_NOT_CONFIGURED']; - yield [MissingAuthenticationException::TYPE, 'INVALID_AUTHORIZATION']; - yield [VerifyAuthenticationException::TYPE, 'INVALID_API_KEY']; + yield [ValidationException::ERROR_CODE, 'INVALID_ARGUMENT']; + yield [DeleteShortUrlException::ERROR_CODE, 'INVALID_SHORT_URL_DELETION']; + yield [DomainNotFoundException::ERROR_CODE, 'DOMAIN_NOT_FOUND']; + yield [ForbiddenTagOperationException::ERROR_CODE, 'FORBIDDEN_OPERATION']; + yield [InvalidUrlException::ERROR_CODE, 'INVALID_URL']; + yield [NonUniqueSlugException::ERROR_CODE, 'INVALID_SLUG']; + yield [ShortUrlNotFoundException::ERROR_CODE, 'INVALID_SHORTCODE']; + yield [TagConflictException::ERROR_CODE, 'TAG_CONFLICT']; + yield [TagNotFoundException::ERROR_CODE, 'TAG_NOT_FOUND']; + yield [MercureException::ERROR_CODE, 'MERCURE_NOT_CONFIGURED']; + yield [MissingAuthenticationException::ERROR_CODE, 'INVALID_AUTHORIZATION']; + yield [VerifyAuthenticationException::ERROR_CODE, 'INVALID_API_KEY']; } } From 750a546faf94b25d878a912f106d65b1d5ae5c28 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 14 Aug 2022 13:18:29 +0200 Subject: [PATCH 036/108] Disabled mutation tests filtering until it properly works --- .github/workflows/ci-mutation-tests.yml | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci-mutation-tests.yml b/.github/workflows/ci-mutation-tests.yml index f152ed15..337894e2 100644 --- a/.github/workflows/ci-mutation-tests.yml +++ b/.github/workflows/ci-mutation-tests.yml @@ -26,13 +26,15 @@ jobs: path: build - name: Resolve infection args id: infection_args - run: | - BRANCH="${GITHUB_REF#refs/heads/}" | - if [[ $BRANCH == 'main' || $BRANCH == 'develop' ]]; then - echo "::set-output name=args::--logger-github=false" - else - echo "::set-output name=args::--logger-github=false --git-diff-lines --git-diff-base=develop" - fi; + run: echo "::set-output name=args::--logger-github=false" +# TODO Try to filter mutation tests to improve execution times. Investigate why --git-diff-lines --git-diff-base=develop does not work +# run: | +# BRANCH="${GITHUB_REF#refs/heads/}" | +# if [[ $BRANCH == 'main' || $BRANCH == 'develop' ]]; then +# echo "::set-output name=args::--logger-github=false" +# else +# echo "::set-output name=args::--logger-github=false --git-diff-lines --git-diff-base=develop" +# fi; shell: bash - if: ${{ inputs.test-group == 'unit' }} run: composer infect:ci:unit -- ${{ steps.infection_args.outputs.args }} From 672b7283790d1b80e0673ca210f59f0e6dc42f01 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 14 Aug 2022 13:55:43 +0200 Subject: [PATCH 037/108] Updated swagger docs, with new API v3 error types --- ...gs.json => short-url-invalid-args-v2.json} | 0 .../examples/short-url-invalid-args-v3.json | 9 ++ ...found.json => short-url-not-found-v2.json} | 0 .../examples/short-url-not-found-v3.json | 9 ++ ...g-not-found.json => tag-not-found-v2.json} | 0 docs/swagger/examples/tag-not-found-v3.json | 9 ++ docs/swagger/paths/v1_short-urls.json | 33 +++++- docs/swagger/paths/v1_short-urls_shorten.json | 34 ++++-- .../paths/v1_short-urls_{shortCode}.json | 56 +++++++--- .../v1_short-urls_{shortCode}_visits.json | 7 +- docs/swagger/paths/v1_tags.json | 104 +++++++++++++----- docs/swagger/paths/v2_domains_redirects.json | 25 ++++- .../paths/v2_domains_{domain}_visits.json | 25 ++++- docs/swagger/paths/v2_mercure-info.json | 22 +++- docs/swagger/paths/v2_tags_{tag}_visits.json | 8 +- 15 files changed, 267 insertions(+), 74 deletions(-) rename docs/swagger/examples/{short-url-invalid-args.json => short-url-invalid-args-v2.json} (100%) create mode 100644 docs/swagger/examples/short-url-invalid-args-v3.json rename docs/swagger/examples/{short-url-not-found.json => short-url-not-found-v2.json} (100%) create mode 100644 docs/swagger/examples/short-url-not-found-v3.json rename docs/swagger/examples/{tag-not-found.json => tag-not-found-v2.json} (100%) create mode 100644 docs/swagger/examples/tag-not-found-v3.json diff --git a/docs/swagger/examples/short-url-invalid-args.json b/docs/swagger/examples/short-url-invalid-args-v2.json similarity index 100% rename from docs/swagger/examples/short-url-invalid-args.json rename to docs/swagger/examples/short-url-invalid-args-v2.json diff --git a/docs/swagger/examples/short-url-invalid-args-v3.json b/docs/swagger/examples/short-url-invalid-args-v3.json new file mode 100644 index 00000000..3e9171c6 --- /dev/null +++ b/docs/swagger/examples/short-url-invalid-args-v3.json @@ -0,0 +1,9 @@ +{ + "value": { + "title": "Invalid data", + "type": "https://shlink.io/api/error/invalid-data", + "detail": "Provided data is not valid", + "status": 400, + "invalidElements": ["maxVisits", "validSince"] + } +} diff --git a/docs/swagger/examples/short-url-not-found.json b/docs/swagger/examples/short-url-not-found-v2.json similarity index 100% rename from docs/swagger/examples/short-url-not-found.json rename to docs/swagger/examples/short-url-not-found-v2.json diff --git a/docs/swagger/examples/short-url-not-found-v3.json b/docs/swagger/examples/short-url-not-found-v3.json new file mode 100644 index 00000000..82f3469c --- /dev/null +++ b/docs/swagger/examples/short-url-not-found-v3.json @@ -0,0 +1,9 @@ +{ + "value": { + "detail": "No URL found with short code \"abc123\"", + "title": "Short URL not found", + "type": "https://shlink.io/api/error/short-url-not-found", + "status": 404, + "shortCode": "abc123" + } +} diff --git a/docs/swagger/examples/tag-not-found.json b/docs/swagger/examples/tag-not-found-v2.json similarity index 100% rename from docs/swagger/examples/tag-not-found.json rename to docs/swagger/examples/tag-not-found-v2.json diff --git a/docs/swagger/examples/tag-not-found-v3.json b/docs/swagger/examples/tag-not-found-v3.json new file mode 100644 index 00000000..62beb42c --- /dev/null +++ b/docs/swagger/examples/tag-not-found-v3.json @@ -0,0 +1,9 @@ +{ + "value": { + "detail": "Tag with name \"foo\" could not be found", + "title": "Tag not found", + "type": "https://shlink.io/api/error/tag-not-found", + "status": 404, + "tag": "foo" + } +} diff --git a/docs/swagger/paths/v1_short-urls.json b/docs/swagger/paths/v1_short-urls.json index 6e8bb015..2675ab61 100644 --- a/docs/swagger/paths/v1_short-urls.json +++ b/docs/swagger/paths/v1_short-urls.json @@ -327,11 +327,11 @@ }, "url": { "type": "string", - "description": "A URL that could not be verified, if the error type is INVALID_URL" + "description": "A URL that could not be verified, if the error type is https://shlink.io/api/error/invalid-url" }, "customSlug": { "type": "string", - "description": "Provided custom slug when the error type is INVALID_SLUG" + "description": "Provided custom slug when the error type is https://shlink.io/api/error/non-unique-slug" }, "domain": { "type": "string", @@ -342,10 +342,31 @@ ] }, "examples": { - "Invalid arguments": { - "$ref": "../examples/short-url-invalid-args.json" + "Invalid arguments with API v3 and newer": { + "$ref": "../examples/short-url-invalid-args-v3.json" }, - "Invalid long URL": { + "Invalid long URL with API v3 and newer": { + "value": { + "title": "Invalid URL", + "type": "https://shlink.io/api/error/invalid-url", + "detail": "Provided URL foo is invalid. Try with a different one.", + "status": 400, + "url": "https://invalid-url.com" + } + }, + "Non-unique slug with API v3 and newer": { + "value": { + "title": "Invalid custom slug", + "type": "https://shlink.io/api/error/non-unique-slug", + "detail": "Provided slug \"my-slug\" is already in use.", + "status": 400, + "customSlug": "my-slug" + } + }, + "Invalid arguments previous to API v3": { + "$ref": "../examples/short-url-invalid-args-v2.json" + }, + "Invalid long URL previous to API v3": { "value": { "title": "Invalid URL", "type": "INVALID_URL", @@ -354,7 +375,7 @@ "url": "https://invalid-url.com" } }, - "Non-unique slug": { + "Non-unique slug previous to API v3": { "value": { "title": "Invalid custom slug", "type": "INVALID_SLUG", diff --git a/docs/swagger/paths/v1_short-urls_shorten.json b/docs/swagger/paths/v1_short-urls_shorten.json index 722476bb..aa26fa1b 100644 --- a/docs/swagger/paths/v1_short-urls_shorten.json +++ b/docs/swagger/paths/v1_short-urls_shorten.json @@ -85,19 +85,39 @@ "schema": { "$ref": "../definitions/Error.json" }, - "example": { - "title": "Invalid URL", - "type": "INVALID_URL", - "detail": "Provided URL foo is invalid. Try with a different one.", - "status": 400, - "url": "https://invalid-url.com" + "examples": { + "API v3 and newer": { + "value": { + "title": "Invalid URL", + "type": "https://shlink.io/api/error/invalid-url", + "detail": "Provided URL foo is invalid. Try with a different one.", + "status": 400, + "url": "https://invalid-url.com" + } + }, + "Previous to API v3": { + "value": { + "title": "Invalid URL", + "type": "INVALID_URL", + "detail": "Provided URL foo is invalid. Try with a different one.", + "status": 400, + "url": "https://invalid-url.com" + } + } } }, "text/plain": { "schema": { "type": "string" }, - "example": "INVALID_URL" + "examples": { + "API v3 and newer": { + "value": "https://shlink.io/api/error/invalid-url" + }, + "Previous to API v3": { + "value": "INVALID_URL" + } + } } } }, diff --git a/docs/swagger/paths/v1_short-urls_{shortCode}.json b/docs/swagger/paths/v1_short-urls_{shortCode}.json index 9065ff89..1b001cc9 100644 --- a/docs/swagger/paths/v1_short-urls_{shortCode}.json +++ b/docs/swagger/paths/v1_short-urls_{shortCode}.json @@ -83,8 +83,11 @@ ] }, "examples": { - "Not found": { - "$ref": "../examples/short-url-not-found.json" + "API v3 and newer": { + "$ref": "../examples/short-url-not-found-v3.json" + }, + "Previous to API v3": { + "$ref": "../examples/short-url-not-found-v2.json" } } } @@ -203,8 +206,11 @@ ] }, "examples": { - "Invalid arguments": { - "$ref": "../examples/short-url-invalid-args.json" + "API v3 and newer": { + "$ref": "../examples/short-url-invalid-args-v3.json" + }, + "Previous to API v3": { + "$ref": "../examples/short-url-invalid-args-v2.json" } } } @@ -236,8 +242,11 @@ ] }, "examples": { - "Not found": { - "$ref": "../examples/short-url-not-found.json" + "API v3 and newer": { + "$ref": "../examples/short-url-not-found-v3.json" + }, + "Previous to API v3": { + "$ref": "../examples/short-url-not-found-v2.json" } } } @@ -318,13 +327,27 @@ } ] }, - "example": { - "title": "Cannot delete short URL", - "type": "INVALID_SHORT_URL_DELETION", - "detail": "Impossible to delete short URL with short code \"abc123\", since it has more than \"15\" visits.", - "status": 422, - "shortCode": "abc123", - "threshold": 15 + "examples": { + "API v3 and newer": { + "value": { + "title": "Cannot delete short URL", + "type": "https://shlink.io/api/error/invalid-short-url-deletion", + "detail": "Impossible to delete short URL with short code \"abc123\", since it has more than \"15\" visits.", + "status": 422, + "shortCode": "abc123", + "threshold": 15 + } + }, + "Previous to API v3": { + "value": { + "title": "Cannot delete short URL", + "type": "INVALID_SHORT_URL_DELETION", + "detail": "Impossible to delete short URL with short code \"abc123\", since it has more than \"15\" visits.", + "status": 422, + "shortCode": "abc123", + "threshold": 15 + } + } } } } @@ -355,8 +378,11 @@ ] }, "examples": { - "Not found": { - "$ref": "../examples/short-url-not-found.json" + "API v3 and newer": { + "$ref": "../examples/short-url-not-found-v3.json" + }, + "Previous to API v3": { + "$ref": "../examples/short-url-not-found-v2.json" } } } diff --git a/docs/swagger/paths/v1_short-urls_{shortCode}_visits.json b/docs/swagger/paths/v1_short-urls_{shortCode}_visits.json index 08a93b68..e86bb698 100644 --- a/docs/swagger/paths/v1_short-urls_{shortCode}_visits.json +++ b/docs/swagger/paths/v1_short-urls_{shortCode}_visits.json @@ -151,8 +151,11 @@ "$ref": "../definitions/Error.json" }, "examples": { - "Short URL not found": { - "$ref": "../examples/short-url-not-found.json" + "Short URL not found with API v3 and newer": { + "$ref": "../examples/short-url-not-found-v3.json" + }, + "Short URL not found previous to API v3": { + "$ref": "../examples/short-url-not-found-v2.json" } } } diff --git a/docs/swagger/paths/v1_tags.json b/docs/swagger/paths/v1_tags.json index a8219bf1..0e77cf3c 100644 --- a/docs/swagger/paths/v1_tags.json +++ b/docs/swagger/paths/v1_tags.json @@ -188,12 +188,25 @@ "schema": { "$ref": "../definitions/Error.json" }, - "example": { - "title": "Invalid data", - "type": "INVALID_ARGUMENT", - "detail": "Provided data is not valid", - "status": 400, - "invalidElements": ["oldName", "newName"] + "examples": { + "API v3 and newer": { + "value": { + "title": "Invalid data", + "type": "https://shlink.io/api/error/invalid-data", + "detail": "Provided data is not valid", + "status": 400, + "invalidElements": ["oldName", "newName"] + } + }, + "Previous to API v3": { + "value": { + "title": "Invalid data", + "type": "INVALID_ARGUMENT", + "detail": "Provided data is not valid", + "status": 400, + "invalidElements": ["oldName", "newName"] + } + } } } } @@ -205,11 +218,23 @@ "schema": { "$ref": "../definitions/Error.json" }, - "example": { - "detail": "You are not allowed to rename tags", - "title": "Forbidden tag operation", - "type": "FORBIDDEN_OPERATION", - "status": 403 + "examples": { + "API v3 and newer": { + "value": { + "detail": "You are not allowed to rename tags", + "title": "Forbidden tag operation", + "type": "https://shlink.io/api/error/forbidden-tag-operation", + "status": 403 + } + }, + "Previous to API v3": { + "value": { + "detail": "You are not allowed to rename tags", + "title": "Forbidden tag operation", + "type": "FORBIDDEN_OPERATION", + "status": 403 + } + } } } } @@ -222,8 +247,11 @@ "$ref": "../definitions/Error.json" }, "examples": { - "Tag not found": { - "$ref": "../examples/tag-not-found.json" + "API v3 and newer": { + "$ref": "../examples/tag-not-found-v3.json" + }, + "Previous to API v3": { + "$ref": "../examples/tag-not-found-v2.json" } } } @@ -236,13 +264,27 @@ "schema": { "$ref": "../definitions/Error.json" }, - "example": { - "detail": "You cannot rename tag foo, because it already exists", - "title": "Tag conflict", - "type": "TAG_CONFLICT", - "status": 409, - "oldName": "bar", - "newName": "foo" + "examples": { + "API v3 and newer": { + "value": { + "detail": "You cannot rename tag foo, because it already exists", + "title": "Tag conflict", + "type": "https://shlink.io/api/error/tag-conflict", + "status": 409, + "oldName": "bar", + "newName": "foo" + } + }, + "Previous to API v3": { + "value": { + "detail": "You cannot rename tag foo, because it already exists", + "title": "Tag conflict", + "type": "TAG_CONFLICT", + "status": 409, + "oldName": "bar", + "newName": "foo" + } + } } } } @@ -300,11 +342,23 @@ "schema": { "$ref": "../definitions/Error.json" }, - "example": { - "detail": "You are not allowed to delete tags", - "title": "Forbidden tag operation", - "type": "FORBIDDEN_OPERATION", - "status": 403 + "examples": { + "API v3 and newer": { + "value": { + "detail": "You are not allowed to delete tags", + "title": "Forbidden tag operation", + "type": "https://shlink.io/api/error/forbidden-tag-operation", + "status": 403 + } + }, + "Previous to API v3": { + "value": { + "detail": "You are not allowed to delete tags", + "title": "Forbidden tag operation", + "type": "FORBIDDEN_OPERATION", + "status": 403 + } + } } } } diff --git a/docs/swagger/paths/v2_domains_redirects.json b/docs/swagger/paths/v2_domains_redirects.json index d4d4338c..cc328040 100644 --- a/docs/swagger/paths/v2_domains_redirects.json +++ b/docs/swagger/paths/v2_domains_redirects.json @@ -94,12 +94,25 @@ } ] }, - "example": { - "title": "Invalid data", - "type": "INVALID_ARGUMENT", - "detail": "Provided data is not valid", - "status": 400, - "invalidElements": ["domain", "invalidShortUrlRedirect"] + "examples": { + "API v3 and newer": { + "value": { + "title": "Invalid data", + "type": "https://shlink.io/api/error/invalid-data", + "detail": "Provided data is not valid", + "status": 400, + "invalidElements": ["domain", "invalidShortUrlRedirect"] + } + }, + "Previous to API v3": { + "value": { + "title": "Invalid data", + "type": "INVALID_ARGUMENT", + "detail": "Provided data is not valid", + "status": 400, + "invalidElements": ["domain", "invalidShortUrlRedirect"] + } + } } } } diff --git a/docs/swagger/paths/v2_domains_{domain}_visits.json b/docs/swagger/paths/v2_domains_{domain}_visits.json index 33389f32..d3acf60e 100644 --- a/docs/swagger/paths/v2_domains_{domain}_visits.json +++ b/docs/swagger/paths/v2_domains_{domain}_visits.json @@ -147,12 +147,25 @@ "schema": { "$ref": "../definitions/Error.json" }, - "example": { - "detail": "Domain with authority \"example.com\" could not be found", - "title": "Domain not found", - "type": "DOMAIN_NOT_FOUND", - "status": 404, - "authority": "example.com" + "examples": { + "API v3 and newer": { + "value": { + "detail": "Domain with authority \"example.com\" could not be found", + "title": "Domain not found", + "type": "https://shlink.io/api/error/domain-not-found", + "status": 404, + "authority": "example.com" + } + }, + "Previous to API v3": { + "value": { + "detail": "Domain with authority \"example.com\" could not be found", + "title": "Domain not found", + "type": "DOMAIN_NOT_FOUND", + "status": 404, + "authority": "example.com" + } + } } } } diff --git a/docs/swagger/paths/v2_mercure-info.json b/docs/swagger/paths/v2_mercure-info.json index a341573f..e637ca33 100644 --- a/docs/swagger/paths/v2_mercure-info.json +++ b/docs/swagger/paths/v2_mercure-info.json @@ -39,11 +39,23 @@ "schema": { "$ref": "../definitions/Error.json" }, - "example": { - "title": "Mercure integration not configured", - "type": "MERCURE_NOT_CONFIGURED", - "detail": "This Shlink instance is not integrated with a mercure hub.", - "status": 501 + "examples": { + "API v3 and newer": { + "value": { + "title": "Mercure integration not configured", + "type": "https://shlink.io/api/error/mercure-not-configured", + "detail": "This Shlink instance is not integrated with a mercure hub.", + "status": 501 + } + }, + "Previous to API v3": { + "value": { + "title": "Mercure integration not configured", + "type": "MERCURE_NOT_CONFIGURED", + "detail": "This Shlink instance is not integrated with a mercure hub.", + "status": 501 + } + } } } } diff --git a/docs/swagger/paths/v2_tags_{tag}_visits.json b/docs/swagger/paths/v2_tags_{tag}_visits.json index 109cb1d0..d40b7020 100644 --- a/docs/swagger/paths/v2_tags_{tag}_visits.json +++ b/docs/swagger/paths/v2_tags_{tag}_visits.json @@ -148,8 +148,12 @@ "$ref": "../definitions/Error.json" }, "examples": { - "Tag not found": { - "$ref": "../examples/tag-not-found.json" + + "API v3 and newer": { + "$ref": "../examples/tag-not-found-v3.json" + }, + "Previous to API v3": { + "$ref": "../examples/tag-not-found-v2.json" } } } From 39c71638e65429db1f936babbb9cee7dcbdf7792 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 14 Aug 2022 14:02:09 +0200 Subject: [PATCH 038/108] Updated changelog --- CHANGELOG.md | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 202e1ba2..b2ec6b82 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,26 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this ## [Unreleased] ### Added -* *Nothing* +* [#1406](https://github.com/shlinkio/shlink/issues/1406) Added new REST API version 3. + + When making requests to the REST API with `/rest/v3/...` and an error occurs, all error types will be different, with the next correlation: + + * `INVALID_ARGUMENT` -> `https://shlink.io/api/error/invalid-data` + * `INVALID_SHORT_URL_DELETION` -> `https://shlink.io/api/error/invalid-short-url-deletion` + * `DOMAIN_NOT_FOUND` -> `https://shlink.io/api/error/domain-not-found` + * `FORBIDDEN_OPERATION` -> `https://shlink.io/api/error/forbidden-tag-operation` + * `INVALID_URL` -> `https://shlink.io/api/error/invalid-url` + * `INVALID_SLUG` -> `https://shlink.io/api/error/non-unique-slug` + * `INVALID_SHORTCODE` -> `https://shlink.io/api/error/short-url-not-found` + * `TAG_CONFLICT` -> `https://shlink.io/api/error/tag-conflict` + * `TAG_NOT_FOUND` -> `https://shlink.io/api/error/tag-not-found` + * `MERCURE_NOT_CONFIGURED` -> `https://shlink.io/api/error/mercure-not-configured` + * `INVALID_AUTHORIZATION` -> `https://shlink.io/api/error/missing-authentication` + * `INVALID_API_KEY` -> `https://shlink.io/api/error/invalid-api-key` + + If you make a request to the API with v2 or v1, the old error types will be returned, until Shlink 4 is released, when only the new ones will be used. + + Non-error responses are not affected. ### Changed * [#1339](https://github.com/shlinkio/shlink/issues/1339) Added new test suite for CLI E2E tests. From cf0fc956c9ea210e54b4ddf15e64910e4fc828e1 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 14 Aug 2022 17:05:13 +0200 Subject: [PATCH 039/108] Added publishing of the docker image in GHCR --- .github/workflows/docker-image-build.yml | 14 ++++++++++---- docker/build | 6 +++--- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/.github/workflows/docker-image-build.yml b/.github/workflows/docker-image-build.yml index a7581b5f..63020514 100644 --- a/.github/workflows/docker-image-build.yml +++ b/.github/workflows/docker-image-build.yml @@ -14,15 +14,21 @@ jobs: - name: Checkout code uses: actions/checkout@v3 - name: Set up QEMU - uses: docker/setup-qemu-action@v1 + uses: docker/setup-qemu-action@v2 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1 + uses: docker/setup-buildx-action@v2 with: version: latest - name: Login to docker hub - uses: docker/login-action@v1 + uses: docker/login-action@v2 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - - name: Build the image + - name: Login to GitHub container registry + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GHCR_PAT }} + - name: Build and push the image run: bash ./docker/build diff --git a/docker/build b/docker/build index fdd58106..5d59bd9d 100755 --- a/docker/build +++ b/docker/build @@ -8,9 +8,9 @@ DOCKER_IMAGE="shlinkio/shlink" # If ref is not develop, then this is a tag. Build that docker tag and also "stable" if [[ "$GITHUB_REF" != *"develop"* ]]; then VERSION=${GITHUB_REF#refs/tags/v} - TAGS="-t ${DOCKER_IMAGE}:${VERSION}" + TAGS="-t ${DOCKER_IMAGE}:${VERSION} -t ghcr.io/${DOCKER_IMAGE}:${VERSION}" # Push stable tag only if this is not an alpha or beta tag - [[ $GITHUB_REF != *"alpha"* && $GITHUB_REF != *"beta"* ]] && TAGS="${TAGS} -t ${DOCKER_IMAGE}:stable" + [[ $GITHUB_REF != *"alpha"* && $GITHUB_REF != *"beta"* ]] && TAGS="${TAGS} -t ${DOCKER_IMAGE}:stable -t ghcr.io/${DOCKER_IMAGE}:stable" docker buildx build --push \ --build-arg SHLINK_VERSION=${VERSION} \ @@ -21,5 +21,5 @@ if [[ "$GITHUB_REF" != *"develop"* ]]; then elif [[ "$GITHUB_REF" == *"develop"* ]]; then docker buildx build --push \ --platform ${PLATFORMS} \ - -t ${DOCKER_IMAGE}:latest . + -t ${DOCKER_IMAGE}:latest -t ghcr.io/${DOCKER_IMAGE}:latest . fi From 5c2061a6e685aa58e55dba12f0f97451a48b6ecf Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 14 Aug 2022 17:07:07 +0200 Subject: [PATCH 040/108] Updated changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b2ec6b82..691af5de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this Non-error responses are not affected. +* [#1513](https://github.com/shlinkio/shlink/issues/1513) Added publishing of the docker image in GHCR. + ### Changed * [#1339](https://github.com/shlinkio/shlink/issues/1339) Added new test suite for CLI E2E tests. * [#1503](https://github.com/shlinkio/shlink/issues/1503) Drastically improved build time in GitHub Actions, by optimizing parallelization, adding php extensions cache and running mutation tests only for changed files. From a9c6a12182e659a43b020b5e5daec0c85f064346 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 14 Aug 2022 17:36:58 +0200 Subject: [PATCH 041/108] Migrated docker build to a reusable workflow --- .github/workflows/docker-image-build.yml | 29 ++++-------------------- 1 file changed, 5 insertions(+), 24 deletions(-) diff --git a/.github/workflows/docker-image-build.yml b/.github/workflows/docker-image-build.yml index 63020514..21e51ff4 100644 --- a/.github/workflows/docker-image-build.yml +++ b/.github/workflows/docker-image-build.yml @@ -1,4 +1,4 @@ -name: Build docker image +name: Build and publish docker image on: push: @@ -9,26 +9,7 @@ on: jobs: build: - runs-on: ubuntu-22.04 - steps: - - name: Checkout code - uses: actions/checkout@v3 - - name: Set up QEMU - uses: docker/setup-qemu-action@v2 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 - with: - version: latest - - name: Login to docker hub - uses: docker/login-action@v2 - with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_PASSWORD }} - - name: Login to GitHub container registry - uses: docker/login-action@v2 - with: - registry: ghcr.io - username: ${{ github.repository_owner }} - password: ${{ secrets.GHCR_PAT }} - - name: Build and push the image - run: bash ./docker/build + uses: shlinkio/github-actions/.github/workflows/docker-build-and-publish.yml@main + secrets: inherit + with: + build-and-publish-script: bash ./docker/build From dbca5b2a7efe01288c5be7e82483e0931fbd6212 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Wed, 17 Aug 2022 19:07:39 +0200 Subject: [PATCH 042/108] Ensured every mutation test job only downloads the specific coverage report --- .github/workflows/ci-mutation-tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci-mutation-tests.yml b/.github/workflows/ci-mutation-tests.yml index 337894e2..7a9dd751 100644 --- a/.github/workflows/ci-mutation-tests.yml +++ b/.github/workflows/ci-mutation-tests.yml @@ -23,6 +23,7 @@ jobs: extensions-cache-key: mutation-tests-extensions-${{ matrix.php-version }}-${{ inputs.test-group }} - uses: actions/download-artifact@v3 with: + name: coverage-${{ inputs.test-group }} path: build - name: Resolve infection args id: infection_args From 60ece7fbf7c3eb2feeb8940f49c13ba02ea793b4 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Fri, 19 Aug 2022 15:11:46 +0200 Subject: [PATCH 043/108] Moved to docker build on reusable workflow --- .github/workflows/docker-image-build.yml | 3 ++- docker/build | 25 ------------------------ 2 files changed, 2 insertions(+), 26 deletions(-) delete mode 100755 docker/build diff --git a/.github/workflows/docker-image-build.yml b/.github/workflows/docker-image-build.yml index 21e51ff4..cbd8b213 100644 --- a/.github/workflows/docker-image-build.yml +++ b/.github/workflows/docker-image-build.yml @@ -12,4 +12,5 @@ jobs: uses: shlinkio/github-actions/.github/workflows/docker-build-and-publish.yml@main secrets: inherit with: - build-and-publish-script: bash ./docker/build + image-name: shlinkio/shlink + version-arg-name: SHLINK_VERSION diff --git a/docker/build b/docker/build deleted file mode 100755 index 5d59bd9d..00000000 --- a/docker/build +++ /dev/null @@ -1,25 +0,0 @@ -#!/bin/bash - -set -ex - -PLATFORMS="linux/arm/v7,linux/arm64/v8,linux/amd64" -DOCKER_IMAGE="shlinkio/shlink" - -# If ref is not develop, then this is a tag. Build that docker tag and also "stable" -if [[ "$GITHUB_REF" != *"develop"* ]]; then - VERSION=${GITHUB_REF#refs/tags/v} - TAGS="-t ${DOCKER_IMAGE}:${VERSION} -t ghcr.io/${DOCKER_IMAGE}:${VERSION}" - # Push stable tag only if this is not an alpha or beta tag - [[ $GITHUB_REF != *"alpha"* && $GITHUB_REF != *"beta"* ]] && TAGS="${TAGS} -t ${DOCKER_IMAGE}:stable -t ghcr.io/${DOCKER_IMAGE}:stable" - - docker buildx build --push \ - --build-arg SHLINK_VERSION=${VERSION} \ - --platform ${PLATFORMS} \ - ${TAGS} . - -# If build branch is develop, build latest -elif [[ "$GITHUB_REF" == *"develop"* ]]; then - docker buildx build --push \ - --platform ${PLATFORMS} \ - -t ${DOCKER_IMAGE}:latest -t ghcr.io/${DOCKER_IMAGE}:latest . -fi From 4882bec11846717c2274078ce942d9829fbc3c5b Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 21 Aug 2022 13:19:27 +0200 Subject: [PATCH 044/108] Added roadrunner to the project --- .gitignore | 2 + bin/roadrunner-worker.php | 31 +++++++++ composer.json | 4 +- config/autoload/mercure.local.php.dist | 2 +- config/autoload/url-shortener.local.php.dist | 8 ++- config/config.php | 5 +- config/roadrunner/.rr.dev.yml | 41 +++++++++++ config/roadrunner/.rr.yml | 32 +++++++++ data/infra/roadrunner.Dockerfile | 73 ++++++++++++++++++++ docker-compose.override.yml.dist | 6 ++ docker-compose.yml | 28 +++++++- 11 files changed, 224 insertions(+), 8 deletions(-) create mode 100644 bin/roadrunner-worker.php create mode 100644 config/roadrunner/.rr.dev.yml create mode 100644 config/roadrunner/.rr.yml create mode 100644 data/infra/roadrunner.Dockerfile diff --git a/.gitignore b/.gitignore index 933c25ee..4154e11b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ .idea +bin/.rr.* +bin/rr build !docker/build composer.lock diff --git a/bin/roadrunner-worker.php b/bin/roadrunner-worker.php new file mode 100644 index 00000000..a8d3506a --- /dev/null +++ b/bin/roadrunner-worker.php @@ -0,0 +1,31 @@ +get(Application::class); + $worker = new PSR7Worker( + Worker::create(), + $container->get(ServerRequestFactoryInterface::class), + $container->get(StreamFactoryInterface::class), + $container->get(UploadedFileFactoryInterface::class), + ); + + while ($req = $worker->waitRequest()) { + try { + $worker->respond($app->handle($req)); + } catch (Throwable $throwable) { + $worker->getWorker()->error((string) $throwable); + } + } +})(); diff --git a/composer.json b/composer.json index 168418a5..f9d3e27a 100644 --- a/composer.json +++ b/composer.json @@ -44,11 +44,13 @@ "pugx/shortid-php": "^1.0", "ramsey/uuid": "^4.3", "shlinkio/shlink-common": "^5.0", - "shlinkio/shlink-config": "^2.0", + "shlinkio/shlink-config": "dev-main#24ccd64 as 2.1", "shlinkio/shlink-event-dispatcher": "^2.5", "shlinkio/shlink-importer": "^4.0", "shlinkio/shlink-installer": "^8.1", "shlinkio/shlink-ip-geolocation": "^3.0", + "spiral/roadrunner": "^2.11", + "spiral/roadrunner-jobs": "^2.3", "symfony/console": "^6.1", "symfony/filesystem": "^6.1", "symfony/lock": "^6.1", diff --git a/config/autoload/mercure.local.php.dist b/config/autoload/mercure.local.php.dist index b10ad86e..e818404b 100644 --- a/config/autoload/mercure.local.php.dist +++ b/config/autoload/mercure.local.php.dist @@ -7,7 +7,7 @@ return [ 'mercure' => [ 'public_hub_url' => 'http://localhost:8001', 'internal_hub_url' => 'http://shlink_mercure_proxy', - 'jwt_secret' => 'mercure_jwt_key', + 'jwt_secret' => 'mercure_jwt_key_long_enough_to_avoid_error', ], ]; diff --git a/config/autoload/url-shortener.local.php.dist b/config/autoload/url-shortener.local.php.dist index 0069ffa9..1fffcd0d 100644 --- a/config/autoload/url-shortener.local.php.dist +++ b/config/autoload/url-shortener.local.php.dist @@ -2,14 +2,18 @@ declare(strict_types=1); -$isSwoole = extension_loaded('openswoole'); +use function Shlinkio\Shlink\Config\swooleIsInstalled; return [ 'url_shortener' => [ 'domain' => [ 'schema' => 'http', - 'hostname' => sprintf('localhost:%s', $isSwoole ? '8080' : '8000'), + 'hostname' => sprintf('localhost:%s', match (true) { + swooleIsInstalled() => '8080', // Swoole + PHP_SAPI === 'cli' => '8800', // Roadrunner + default => '8000', // FPM + }), ], 'auto_resolve_titles' => true, // 'multi_segment_slugs_enabled' => true, diff --git a/config/config.php b/config/config.php index 6c38707d..2763d23d 100644 --- a/config/config.php +++ b/config/config.php @@ -13,10 +13,11 @@ use Shlinkio\Shlink\Config\ConfigAggregator\EnvVarLoaderProvider; use function class_exists; use function Shlinkio\Shlink\Config\env; +use function Shlinkio\Shlink\Config\swooleIsInstalled; use const PHP_SAPI; -$isCli = PHP_SAPI === 'cli'; +$enableSwoole = PHP_SAPI === 'cli' && swooleIsInstalled(); $isTestEnv = env('APP_ENV') === 'test'; return (new ConfigAggregator\ConfigAggregator([ @@ -26,7 +27,7 @@ return (new ConfigAggregator\ConfigAggregator([ Mezzio\ConfigProvider::class, Mezzio\Router\ConfigProvider::class, Mezzio\Router\FastRouteRouter\ConfigProvider::class, - $isCli && class_exists(Swoole\ConfigProvider::class) + $enableSwoole && class_exists(Swoole\ConfigProvider::class) ? Swoole\ConfigProvider::class : new ConfigAggregator\ArrayProvider([]), ProblemDetails\ConfigProvider::class, diff --git a/config/roadrunner/.rr.dev.yml b/config/roadrunner/.rr.dev.yml new file mode 100644 index 00000000..af667e0a --- /dev/null +++ b/config/roadrunner/.rr.dev.yml @@ -0,0 +1,41 @@ +version: '2.7' + +server: + command: 'php ../../bin/roadrunner-worker.php' + +http: + address: '0.0.0.0:8080' + static: + dir: ../../public + forbid: + - .php + - .htaccess + pool: + num_workers: 1 + supervisor: + max_worker_memory: 100 + +jobs: + pool: + num_workers: 2 + max_worker_memory: 100 + consume: { } + +logs: + mode: development + channels: + http: + level: debug # Log all http requests, set to info to disable + server: + level: debug # Everything written to worker stderr is logged + metrics: + level: debug + +reload: + enabled: true + interval: 1s + patterns: ['.php', '.yml', '.yaml'] + services: + http: + dirs: ['.'] + recursive: true diff --git a/config/roadrunner/.rr.yml b/config/roadrunner/.rr.yml new file mode 100644 index 00000000..ebfe9673 --- /dev/null +++ b/config/roadrunner/.rr.yml @@ -0,0 +1,32 @@ +version: '2.7' + +server: + command: 'php -dopcache.enable_cli=1 -dopcache.validate_timestamps=0 ../../bin/roadrunner-worker.php' + +http: + address: '0.0.0.0:8080' + static: + dir: ../../public + forbid: + - .php + - .htaccess + pool: + num_workers: 16 # TODO Make configurable + supervisor: + max_worker_memory: 100 + +jobs: + pool: + num_workers: 16 # TODO Make configurable + max_worker_memory: 100 + consume: { } + +logs: + mode: production + channels: + http: + level: info # Log all http requests, set to info to disable + server: + level: debug # Everything written to worker stderr is logged + metrics: + level: error diff --git a/data/infra/roadrunner.Dockerfile b/data/infra/roadrunner.Dockerfile new file mode 100644 index 00000000..7a68a7ae --- /dev/null +++ b/data/infra/roadrunner.Dockerfile @@ -0,0 +1,73 @@ +FROM php:8.1.9-alpine3.16 +MAINTAINER Alejandro Celaya + +ENV APCU_VERSION 5.1.21 +ENV PDO_SQLSRV_VERSION 5.10.1 +ENV MS_ODBC_SQL_VERSION 17.5.2.2 + +RUN apk update + +# Install common php extensions +RUN docker-php-ext-install pdo_mysql +RUN docker-php-ext-install calendar + +RUN apk add --no-cache oniguruma-dev +RUN docker-php-ext-install mbstring + +RUN apk add --no-cache sqlite-libs +RUN apk add --no-cache sqlite-dev +RUN docker-php-ext-install pdo_sqlite + +RUN apk add --no-cache icu-dev +RUN docker-php-ext-install intl + +RUN apk add --no-cache libzip-dev zlib-dev +RUN docker-php-ext-install zip + +RUN apk add --no-cache libpng-dev +RUN docker-php-ext-install gd + +RUN apk add --no-cache postgresql-dev +RUN docker-php-ext-install pdo_pgsql + +RUN docker-php-ext-install sockets +RUN docker-php-ext-install bcmath + +# Install APCu extension +ADD https://pecl.php.net/get/apcu-$APCU_VERSION.tgz /tmp/apcu.tar.gz +RUN mkdir -p /usr/src/php/ext/apcu \ + && tar xf /tmp/apcu.tar.gz -C /usr/src/php/ext/apcu --strip-components=1 \ + && docker-php-ext-configure apcu \ + && docker-php-ext-install apcu \ + && rm /tmp/apcu.tar.gz \ + && rm /usr/local/etc/php/conf.d/docker-php-ext-apcu.ini \ + && echo extension=apcu.so > /usr/local/etc/php/conf.d/20-php-ext-apcu.ini + +# Install pcov and sqlsrv driver +RUN wget https://download.microsoft.com/download/e/4/e/e4e67866-dffd-428c-aac7-8d28ddafb39b/msodbcsql17_${MS_ODBC_SQL_VERSION}-1_amd64.apk && \ + apk add --allow-untrusted msodbcsql17_${MS_ODBC_SQL_VERSION}-1_amd64.apk && \ + apk add --no-cache --virtual .phpize-deps $PHPIZE_DEPS unixodbc-dev && \ + pecl install pdo_sqlsrv-${PDO_SQLSRV_VERSION} pcov && \ + docker-php-ext-enable pdo_sqlsrv pcov && \ + apk del .phpize-deps && \ + rm msodbcsql17_${MS_ODBC_SQL_VERSION}-1_amd64.apk + +# Install composer +COPY --from=composer:2 /usr/bin/composer /usr/local/bin/composer + +# Make home directory writable by anyone +RUN chmod 777 /home + +VOLUME /home/shlink +WORKDIR /home/shlink + +# Expose roadrunner port +EXPOSE 8080 + +CMD \ + # Install dependencies if the vendor dir does not exist + if [[ ! -d "./vendor" ]]; then /usr/local/bin/composer install ; fi && \ + # Download roadrunner binary + if [[ ! -f "./bin/rr" ]]; then ./vendor/bin/rr get --location bin/ && chmod +x bin/rr ; fi && \ + # This forces the app to be started every second until the exit code is 0 + until ./bin/rr serve -c config/roadrunner/.rr.dev.yml; do sleep 1 ; done diff --git a/docker-compose.override.yml.dist b/docker-compose.override.yml.dist index 990d1b5d..1c5409c6 100644 --- a/docker-compose.override.yml.dist +++ b/docker-compose.override.yml.dist @@ -13,6 +13,12 @@ services: - /etc/passwd:/etc/passwd:ro - /etc/group:/etc/group:ro + shlink_roadrunner: + user: 1000:1000 + volumes: + - /etc/passwd:/etc/passwd:ro + - /etc/group:/etc/group:ro + shlink_db_mysql: user: 1000:1000 volumes: diff --git a/docker-compose.yml b/docker-compose.yml index 739c0079..8293ab03 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -73,6 +73,30 @@ services: extra_hosts: - 'host.docker.internal:host-gateway' + shlink_roadrunner: + container_name: shlink_roadrunner + build: + context: . + dockerfile: ./data/infra/roadrunner.Dockerfile + ports: + - "8800:8080" + volumes: + - ./:/home/shlink + - ./data/infra/php.ini:/usr/local/etc/php/php.ini + links: + - shlink_db_mysql + - shlink_db_postgres + - shlink_db_maria + - shlink_db_ms + - shlink_redis + - shlink_mercure + - shlink_mercure_proxy + - shlink_rabbitmq + environment: + LC_ALL: C + extra_hosts: + - 'host.docker.internal:host-gateway' + shlink_db_mysql: container_name: shlink_db_mysql image: mysql:5.7 @@ -144,8 +168,8 @@ services: - "3080:80" environment: SERVER_NAME: ":80" - MERCURE_PUBLISHER_JWT_KEY: mercure_jwt_key - MERCURE_SUBSCRIBER_JWT_KEY: mercure_jwt_key + MERCURE_PUBLISHER_JWT_KEY: mercure_jwt_key_long_enough_to_avoid_error + MERCURE_SUBSCRIBER_JWT_KEY: mercure_jwt_key_long_enough_to_avoid_error MERCURE_EXTRA_DIRECTIVES: "cors_origins https://app.shlink.io http://localhost:3000 http://127.0.0.1:3000" shlink_rabbitmq: From e9ec32b3c3ba2e33c21d8c4e823323b6b74a196d Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Fri, 26 Aug 2022 14:59:27 +0200 Subject: [PATCH 045/108] Added support to dispatch async event listeners as RoadRunner jobs --- .dockerignore | 1 + bin/roadrunner-worker.php | 32 ++++++++++++------- composer.json | 2 +- config/roadrunner/.rr.dev.yml | 18 ++++++++--- config/roadrunner/.rr.yml | 12 ++++++- .../Event/AbstractVisitEvent.php | 8 ++++- .../EventDispatcher/Event/ShortUrlCreated.php | 8 ++++- 7 files changed, 61 insertions(+), 20 deletions(-) diff --git a/.dockerignore b/.dockerignore index 870f3610..ac530ed3 100644 --- a/.dockerignore +++ b/.dockerignore @@ -22,4 +22,5 @@ infection* **/test* build* **/.* +!config/roadrunner/.rr.yml bin/helper diff --git a/bin/roadrunner-worker.php b/bin/roadrunner-worker.php index a8d3506a..7d65f250 100644 --- a/bin/roadrunner-worker.php +++ b/bin/roadrunner-worker.php @@ -7,25 +7,33 @@ use Psr\Container\ContainerInterface; use Psr\Http\Message\ServerRequestFactoryInterface; use Psr\Http\Message\StreamFactoryInterface; use Psr\Http\Message\UploadedFileFactoryInterface; +use Shlinkio\Shlink\EventDispatcher\RoadRunner\RoadRunnerTaskConsumerToListener; use Spiral\RoadRunner\Http\PSR7Worker; use Spiral\RoadRunner\Worker; (static function (): void { + $rrMode = getenv('RR_MODE'); /** @var ContainerInterface $container */ $container = include __DIR__ . '/../config/container.php'; - $app = $container->get(Application::class); - $worker = new PSR7Worker( - Worker::create(), - $container->get(ServerRequestFactoryInterface::class), - $container->get(StreamFactoryInterface::class), - $container->get(UploadedFileFactoryInterface::class), - ); - while ($req = $worker->waitRequest()) { - try { - $worker->respond($app->handle($req)); - } catch (Throwable $throwable) { - $worker->getWorker()->error((string) $throwable); + if ($rrMode === 'http') { + // This was spin-up as a web worker + $app = $container->get(Application::class); + $worker = new PSR7Worker( + Worker::create(), + $container->get(ServerRequestFactoryInterface::class), + $container->get(StreamFactoryInterface::class), + $container->get(UploadedFileFactoryInterface::class), + ); + + while ($req = $worker->waitRequest()) { + try { + $worker->respond($app->handle($req)); + } catch (Throwable $e) { + $worker->getWorker()->error((string) $e); + } } + } else { + $container->get(RoadRunnerTaskConsumerToListener::class)->listenForTasks(); } })(); diff --git a/composer.json b/composer.json index f9d3e27a..e549a8f6 100644 --- a/composer.json +++ b/composer.json @@ -45,7 +45,7 @@ "ramsey/uuid": "^4.3", "shlinkio/shlink-common": "^5.0", "shlinkio/shlink-config": "dev-main#24ccd64 as 2.1", - "shlinkio/shlink-event-dispatcher": "^2.5", + "shlinkio/shlink-event-dispatcher": "dev-feature/roadrunner-support", "shlinkio/shlink-importer": "^4.0", "shlinkio/shlink-installer": "^8.1", "shlinkio/shlink-ip-geolocation": "^3.0", diff --git a/config/roadrunner/.rr.dev.yml b/config/roadrunner/.rr.dev.yml index af667e0a..389ed003 100644 --- a/config/roadrunner/.rr.dev.yml +++ b/config/roadrunner/.rr.dev.yml @@ -1,5 +1,8 @@ version: '2.7' +rpc: + listen: tcp://127.0.0.1:6001 + server: command: 'php ../../bin/roadrunner-worker.php' @@ -11,7 +14,7 @@ http: - .php - .htaccess pool: - num_workers: 1 + num_workers: 3 supervisor: max_worker_memory: 100 @@ -19,15 +22,22 @@ jobs: pool: num_workers: 2 max_worker_memory: 100 - consume: { } + timeout: 300 + consume: ['shlink'] + pipelines: + shlink: + driver: memory + config: + priority: 10 + prefetch: 10 logs: mode: development channels: http: - level: debug # Log all http requests, set to info to disable + level: debug server: - level: debug # Everything written to worker stderr is logged + level: debug metrics: level: debug diff --git a/config/roadrunner/.rr.yml b/config/roadrunner/.rr.yml index ebfe9673..032989b9 100644 --- a/config/roadrunner/.rr.yml +++ b/config/roadrunner/.rr.yml @@ -1,5 +1,8 @@ version: '2.7' +rpc: + listen: tcp://127.0.0.1:6001 + server: command: 'php -dopcache.enable_cli=1 -dopcache.validate_timestamps=0 ../../bin/roadrunner-worker.php' @@ -16,10 +19,17 @@ http: max_worker_memory: 100 jobs: + timeout: 300 pool: num_workers: 16 # TODO Make configurable max_worker_memory: 100 - consume: { } + consume: ['shlink'] + pipelines: + shlink: + driver: memory + config: + priority: 10 + prefetch: 10 logs: mode: production diff --git a/module/Core/src/EventDispatcher/Event/AbstractVisitEvent.php b/module/Core/src/EventDispatcher/Event/AbstractVisitEvent.php index 6fadaa5d..0d41b7d1 100644 --- a/module/Core/src/EventDispatcher/Event/AbstractVisitEvent.php +++ b/module/Core/src/EventDispatcher/Event/AbstractVisitEvent.php @@ -5,8 +5,9 @@ declare(strict_types=1); namespace Shlinkio\Shlink\Core\EventDispatcher\Event; use JsonSerializable; +use Shlinkio\Shlink\EventDispatcher\Util\JsonUnserializable; -abstract class AbstractVisitEvent implements JsonSerializable +abstract class AbstractVisitEvent implements JsonSerializable, JsonUnserializable { public function __construct(public readonly string $visitId) { @@ -16,4 +17,9 @@ abstract class AbstractVisitEvent implements JsonSerializable { return ['visitId' => $this->visitId]; } + + public static function fromPayload(array $payload): self + { + return new static($payload['visitId'] ?? ''); + } } diff --git a/module/Core/src/EventDispatcher/Event/ShortUrlCreated.php b/module/Core/src/EventDispatcher/Event/ShortUrlCreated.php index 9786808f..b6ab1a0c 100644 --- a/module/Core/src/EventDispatcher/Event/ShortUrlCreated.php +++ b/module/Core/src/EventDispatcher/Event/ShortUrlCreated.php @@ -5,8 +5,9 @@ declare(strict_types=1); namespace Shlinkio\Shlink\Core\EventDispatcher\Event; use JsonSerializable; +use Shlinkio\Shlink\EventDispatcher\Util\JsonUnserializable; -final class ShortUrlCreated implements JsonSerializable +final class ShortUrlCreated implements JsonSerializable, JsonUnserializable { public function __construct(public readonly string $shortUrlId) { @@ -18,4 +19,9 @@ final class ShortUrlCreated implements JsonSerializable 'shortUrlId' => $this->shortUrlId, ]; } + + public static function fromPayload(array $payload): self + { + return new self($payload['shortUrlId'] ?? ''); + } } From 846802c00332661d5752c185c8a1ea311acb9201 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Fri, 26 Aug 2022 17:58:25 +0200 Subject: [PATCH 046/108] Slight improvements on RoadRunner config --- config/autoload/url-shortener.local.php.dist | 3 ++- config/roadrunner/.rr.dev.yml | 6 ++++-- .../EventDispatcher/Event/AbstractVisitEvent.php | 2 +- .../Core/src/EventDispatcher/Event/UrlVisited.php | 14 ++++++++++++-- module/Core/src/EventDispatcher/LocateVisit.php | 2 +- module/Core/src/Visit/VisitsTracker.php | 2 +- .../Core/test/EventDispatcher/LocateVisitTest.php | 2 +- .../DropDefaultDomainFromRequestMiddleware.php | 4 ++-- 8 files changed, 24 insertions(+), 11 deletions(-) diff --git a/config/autoload/url-shortener.local.php.dist b/config/autoload/url-shortener.local.php.dist index 1fffcd0d..1ed5a47e 100644 --- a/config/autoload/url-shortener.local.php.dist +++ b/config/autoload/url-shortener.local.php.dist @@ -2,6 +2,7 @@ declare(strict_types=1); +use function Shlinkio\Shlink\Config\env; use function Shlinkio\Shlink\Config\swooleIsInstalled; return [ @@ -10,8 +11,8 @@ return [ 'domain' => [ 'schema' => 'http', 'hostname' => sprintf('localhost:%s', match (true) { + PHP_SAPI === 'cli' && env('RR_MODE') !== null => '8800', // Roadrunner swooleIsInstalled() => '8080', // Swoole - PHP_SAPI === 'cli' => '8800', // Roadrunner default => '8000', // FPM }), ], diff --git a/config/roadrunner/.rr.dev.yml b/config/roadrunner/.rr.dev.yml index 389ed003..4136f195 100644 --- a/config/roadrunner/.rr.dev.yml +++ b/config/roadrunner/.rr.dev.yml @@ -42,10 +42,12 @@ logs: level: debug reload: - enabled: true interval: 1s patterns: ['.php', '.yml', '.yaml'] services: http: - dirs: ['.'] + dirs: ['../..'] + recursive: true + jobs: + dirs: ['../..'] recursive: true diff --git a/module/Core/src/EventDispatcher/Event/AbstractVisitEvent.php b/module/Core/src/EventDispatcher/Event/AbstractVisitEvent.php index 0d41b7d1..907b3d9c 100644 --- a/module/Core/src/EventDispatcher/Event/AbstractVisitEvent.php +++ b/module/Core/src/EventDispatcher/Event/AbstractVisitEvent.php @@ -9,7 +9,7 @@ use Shlinkio\Shlink\EventDispatcher\Util\JsonUnserializable; abstract class AbstractVisitEvent implements JsonSerializable, JsonUnserializable { - public function __construct(public readonly string $visitId) + final public function __construct(public readonly string $visitId) { } diff --git a/module/Core/src/EventDispatcher/Event/UrlVisited.php b/module/Core/src/EventDispatcher/Event/UrlVisited.php index 02452a3e..c57d59d6 100644 --- a/module/Core/src/EventDispatcher/Event/UrlVisited.php +++ b/module/Core/src/EventDispatcher/Event/UrlVisited.php @@ -6,8 +6,18 @@ namespace Shlinkio\Shlink\Core\EventDispatcher\Event; final class UrlVisited extends AbstractVisitEvent { - public function __construct(string $visitId, public readonly ?string $originalIpAddress = null) + private ?string $originalIpAddress = null; + + public static function withOriginalIpAddress(string $visitId, ?string $originalIpAddress): self { - parent::__construct($visitId); + $instance = new self($visitId); + $instance->originalIpAddress = $originalIpAddress; + + return $instance; + } + + public function originalIpAddress(): ?string + { + return $this->originalIpAddress; } } diff --git a/module/Core/src/EventDispatcher/LocateVisit.php b/module/Core/src/EventDispatcher/LocateVisit.php index 197ce9a0..8708fb8a 100644 --- a/module/Core/src/EventDispatcher/LocateVisit.php +++ b/module/Core/src/EventDispatcher/LocateVisit.php @@ -41,7 +41,7 @@ class LocateVisit return; } - $this->locateVisit($visitId, $shortUrlVisited->originalIpAddress, $visit); + $this->locateVisit($visitId, $shortUrlVisited->originalIpAddress(), $visit); $this->eventDispatcher->dispatch(new VisitLocated($visitId)); } diff --git a/module/Core/src/Visit/VisitsTracker.php b/module/Core/src/Visit/VisitsTracker.php index 3aef46df..585a0f86 100644 --- a/module/Core/src/Visit/VisitsTracker.php +++ b/module/Core/src/Visit/VisitsTracker.php @@ -72,6 +72,6 @@ class VisitsTracker implements VisitsTrackerInterface $this->em->persist($visit); $this->em->flush(); - $this->eventDispatcher->dispatch(new UrlVisited($visit->getId(), $visitor->remoteAddress)); + $this->eventDispatcher->dispatch(UrlVisited::withOriginalIpAddress($visit->getId(), $visitor->remoteAddress)); } } diff --git a/module/Core/test/EventDispatcher/LocateVisitTest.php b/module/Core/test/EventDispatcher/LocateVisitTest.php index 406e8146..09a8086d 100644 --- a/module/Core/test/EventDispatcher/LocateVisitTest.php +++ b/module/Core/test/EventDispatcher/LocateVisitTest.php @@ -193,7 +193,7 @@ class LocateVisitTest extends TestCase { $ipAddr = $originalIpAddress ?? $visit->getRemoteAddr(); $location = new Location('', '', '', '', 0.0, 0.0, ''); - $event = new UrlVisited('123', $originalIpAddress); + $event = UrlVisited::withOriginalIpAddress('123', $originalIpAddress); $findVisit = $this->em->find(Visit::class, '123')->willReturn($visit); $flush = $this->em->flush()->will(function (): void { diff --git a/module/Rest/src/Middleware/ShortUrl/DropDefaultDomainFromRequestMiddleware.php b/module/Rest/src/Middleware/ShortUrl/DropDefaultDomainFromRequestMiddleware.php index 8eb98153..73a6ac69 100644 --- a/module/Rest/src/Middleware/ShortUrl/DropDefaultDomainFromRequestMiddleware.php +++ b/module/Rest/src/Middleware/ShortUrl/DropDefaultDomainFromRequestMiddleware.php @@ -11,14 +11,14 @@ use Psr\Http\Server\RequestHandlerInterface; class DropDefaultDomainFromRequestMiddleware implements MiddlewareInterface { - public function __construct(private string $defaultDomain) + public function __construct(private readonly string $defaultDomain) { } public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { /** @var array $body */ - $body = $request->getParsedBody(); + $body = $request->getParsedBody() ?? []; $request = $request->withQueryParams($this->sanitizeDomainFromPayload($request->getQueryParams())) ->withParsedBody($this->sanitizeDomainFromPayload($body)); From 86159c5d8651c57978f939da98cf1e289aa33ee0 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Fri, 26 Aug 2022 19:17:10 +0200 Subject: [PATCH 047/108] Updated to latest event dispatcher lib --- README.md | 2 +- composer.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1fe3b89c..bb99634e 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ A PHP-based self-hosted URL shortener that can be used to serve shortened URLs u - [Full documentation](#full-documentation) - [Docker image](#docker-image) -- [Self hosted](#self-hosted) +- [Self-hosted](#self-hosted) - [Download](#download) - [Configure](#configure) - [Using shlink](#using-shlink) diff --git a/composer.json b/composer.json index e549a8f6..0380442f 100644 --- a/composer.json +++ b/composer.json @@ -45,7 +45,7 @@ "ramsey/uuid": "^4.3", "shlinkio/shlink-common": "^5.0", "shlinkio/shlink-config": "dev-main#24ccd64 as 2.1", - "shlinkio/shlink-event-dispatcher": "dev-feature/roadrunner-support", + "shlinkio/shlink-event-dispatcher": "dev-main#7bf5b58 as 2.6", "shlinkio/shlink-importer": "^4.0", "shlinkio/shlink-installer": "^8.1", "shlinkio/shlink-ip-geolocation": "^3.0", From c5b6d203f53698437623c47d262ac95e5ee43e3b Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 27 Aug 2022 08:01:57 +0200 Subject: [PATCH 048/108] Simplified RoadRunner worker, and fixed RoadRunner reloading config --- bin/roadrunner-worker.php | 15 ++++----------- composer.json | 2 +- config/autoload/dependencies.global.php | 19 +++++++++++++++++++ config/roadrunner/.rr.dev.yml | 4 ++-- 4 files changed, 26 insertions(+), 14 deletions(-) diff --git a/bin/roadrunner-worker.php b/bin/roadrunner-worker.php index 7d65f250..c4a89a85 100644 --- a/bin/roadrunner-worker.php +++ b/bin/roadrunner-worker.php @@ -4,27 +4,20 @@ declare(strict_types=1); use Mezzio\Application; use Psr\Container\ContainerInterface; -use Psr\Http\Message\ServerRequestFactoryInterface; -use Psr\Http\Message\StreamFactoryInterface; -use Psr\Http\Message\UploadedFileFactoryInterface; use Shlinkio\Shlink\EventDispatcher\RoadRunner\RoadRunnerTaskConsumerToListener; use Spiral\RoadRunner\Http\PSR7Worker; -use Spiral\RoadRunner\Worker; + +use function Shlinkio\Shlink\Config\env; (static function (): void { - $rrMode = getenv('RR_MODE'); /** @var ContainerInterface $container */ $container = include __DIR__ . '/../config/container.php'; + $rrMode = env('RR_MODE'); if ($rrMode === 'http') { // This was spin-up as a web worker $app = $container->get(Application::class); - $worker = new PSR7Worker( - Worker::create(), - $container->get(ServerRequestFactoryInterface::class), - $container->get(StreamFactoryInterface::class), - $container->get(UploadedFileFactoryInterface::class), - ); + $worker = $container->get(PSR7Worker::class); while ($req = $worker->waitRequest()) { try { diff --git a/composer.json b/composer.json index 0380442f..0028e27f 100644 --- a/composer.json +++ b/composer.json @@ -45,7 +45,7 @@ "ramsey/uuid": "^4.3", "shlinkio/shlink-common": "^5.0", "shlinkio/shlink-config": "dev-main#24ccd64 as 2.1", - "shlinkio/shlink-event-dispatcher": "dev-main#7bf5b58 as 2.6", + "shlinkio/shlink-event-dispatcher": "dev-main#48c0137 as 2.6", "shlinkio/shlink-importer": "^4.0", "shlinkio/shlink-installer": "^8.1", "shlinkio/shlink-ip-geolocation": "^3.0", diff --git a/config/autoload/dependencies.global.php b/config/autoload/dependencies.global.php index dbc553f1..657caffb 100644 --- a/config/autoload/dependencies.global.php +++ b/config/autoload/dependencies.global.php @@ -3,12 +3,22 @@ declare(strict_types=1); use GuzzleHttp\Client; +use Laminas\ServiceManager\AbstractFactory\ConfigAbstractFactory; use Mezzio\Container; use Psr\Http\Client\ClientInterface; +use Psr\Http\Message\ServerRequestFactoryInterface; +use Psr\Http\Message\StreamFactoryInterface; +use Psr\Http\Message\UploadedFileFactoryInterface; +use Spiral\RoadRunner\Http\PSR7Worker; +use Spiral\RoadRunner\WorkerInterface; return [ 'dependencies' => [ + 'factories' => [ + PSR7Worker::class => ConfigAbstractFactory::class, + ], + 'delegators' => [ Mezzio\Application::class => [ Container\ApplicationConfigInjectionDelegator::class, @@ -26,4 +36,13 @@ return [ ], ], + ConfigAbstractFactory::class => [ + PSR7Worker::class => [ + WorkerInterface::class, + ServerRequestFactoryInterface::class, + StreamFactoryInterface::class, + UploadedFileFactoryInterface::class, + ], + ], + ]; diff --git a/config/roadrunner/.rr.dev.yml b/config/roadrunner/.rr.dev.yml index 4136f195..23e9679f 100644 --- a/config/roadrunner/.rr.dev.yml +++ b/config/roadrunner/.rr.dev.yml @@ -46,8 +46,8 @@ reload: patterns: ['.php', '.yml', '.yaml'] services: http: - dirs: ['../..'] + dirs: ['../../bin', '../../config', '../../data/migrations', '../../docker', '../../module', '../../vendor'] recursive: true jobs: - dirs: ['../..'] + dirs: ['../../bin', '../../config', '../../data/migrations', '../../docker', '../../module', '../../vendor'] recursive: true From ca515998e4dbacadf5cc81745a738cae188517de Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 27 Aug 2022 09:09:14 +0200 Subject: [PATCH 049/108] Added support to run API tests on roadrunner --- .gitignore | 1 + bin/test/run-api-tests.sh | 22 +++++++++++++++++----- composer.json | 1 + config/roadrunner/.rr.dev.yml | 2 +- config/test/bootstrap_api_tests.php | 6 +++--- config/test/constants.php | 4 ++-- config/test/test_config.global.php | 10 +++++----- indocker | 9 +++++++-- 8 files changed, 37 insertions(+), 18 deletions(-) diff --git a/.gitignore b/.gitignore index 4154e11b..daea5f2f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ .idea bin/.rr.* bin/rr +config/roadrunner/.pid build !docker/build composer.lock diff --git a/bin/test/run-api-tests.sh b/bin/test/run-api-tests.sh index 3f6e27e6..1f61b681 100755 --- a/bin/test/run-api-tests.sh +++ b/bin/test/run-api-tests.sh @@ -2,24 +2,36 @@ export APP_ENV=test export DB_DRIVER=postgres export TEST_ENV=api +export TEST_RUNTIME=${TEST_RUNTIME:-"openswoole"} export GENERATE_COVERAGE=${GENERATE_COVERAGE:-"no"} # Reset logs +OUTPUT_LOGS=data/log/api-tests/output.log rm -rf data/log/api-tests mkdir data/log/api-tests -touch data/log/api-tests/output.log +touch $OUTPUT_LOGS # Try to stop server just in case it hanged in last execution -vendor/bin/laminas mezzio:swoole:stop +[[ $TEST_RUNTIME == 'openswoole' ]] && vendor/bin/laminas mezzio:swoole:stop +[[ $TEST_RUNTIME == 'rr' ]] && bin/rr stop -f echo 'Starting server...' -vendor/bin/laminas mezzio:swoole:start -d -sleep 2 +[[ $TEST_RUNTIME == 'openswoole' ]] && vendor/bin/laminas mezzio:swoole:start -d +[[ $TEST_RUNTIME == 'rr' ]] && bin/rr serve -p -c=config/roadrunner/.rr.dev.yml \ + -o=http.address=0.0.0.0:9999 \ + -o=logs.encoding=json \ + -o=logs.channels.http.encoding=json \ + -o=logs.channels.server.encoding=json \ + -o=logs.output="${PWD}/${OUTPUT_LOGS}" \ + -o=logs.channels.http.output="${PWD}/${OUTPUT_LOGS}" \ + -o=logs.channels.server.output="${PWD}/${OUTPUT_LOGS}" & +sleep 2 # Let's give the server a couple of seconds to start vendor/bin/phpunit --order-by=random -c phpunit-api.xml --testdox --colors=always --log-junit=build/coverage-api/junit.xml $* testsExitCode=$? -vendor/bin/laminas mezzio:swoole:stop +[[ $TEST_RUNTIME == 'openswoole' ]] && vendor/bin/laminas mezzio:swoole:stop +[[ $TEST_RUNTIME == 'rr' ]] && bin/rr stop -c config/roadrunner/.rr.dev.yml -o=http.address=0.0.0.0:9999 # Exit this script with the same code as the tests. If tests failed, this script has to fail exit $testsExitCode diff --git a/composer.json b/composer.json index 0028e27f..54d180e3 100644 --- a/composer.json +++ b/composer.json @@ -122,6 +122,7 @@ "test:db:postgres": "DB_DRIVER=postgres composer test:db:sqlite", "test:db:ms": "DB_DRIVER=mssql composer test:db:sqlite", "test:api": "bin/test/run-api-tests.sh", + "test:api:rr": "TEST_RUNTIME=rr bin/test/run-api-tests.sh", "test:api:ci": "GENERATE_COVERAGE=yes composer test:api", "test:api:pretty": "GENERATE_COVERAGE=pretty composer test:api", "test:cli": "APP_ENV=test DB_DRIVER=maria TEST_ENV=cli php vendor/bin/phpunit --order-by=random --colors=always --testdox -c phpunit-cli.xml --log-junit=build/coverage-cli/junit.xml", diff --git a/config/roadrunner/.rr.dev.yml b/config/roadrunner/.rr.dev.yml index 23e9679f..7f06fc9f 100644 --- a/config/roadrunner/.rr.dev.yml +++ b/config/roadrunner/.rr.dev.yml @@ -43,7 +43,7 @@ logs: reload: interval: 1s - patterns: ['.php', '.yml', '.yaml'] + patterns: ['.php'] services: http: dirs: ['../../bin', '../../config', '../../data/migrations', '../../docker', '../../module', '../../vendor'] diff --git a/config/test/bootstrap_api_tests.php b/config/test/bootstrap_api_tests.php index 52c9d4fb..bc119284 100644 --- a/config/test/bootstrap_api_tests.php +++ b/config/test/bootstrap_api_tests.php @@ -10,8 +10,8 @@ use Psr\Container\ContainerInterface; use function register_shutdown_function; use function sprintf; -use const ShlinkioTest\Shlink\SWOOLE_TESTING_HOST; -use const ShlinkioTest\Shlink\SWOOLE_TESTING_PORT; +use const ShlinkioTest\Shlink\API_TESTS_HOST; +use const ShlinkioTest\Shlink\API_TESTS_PORT; /** @var ContainerInterface $container */ $container = require __DIR__ . '/../container.php'; @@ -24,7 +24,7 @@ $httpClient = $container->get('shlink_test_api_client'); register_shutdown_function(function () use ($httpClient): void { $httpClient->request( 'GET', - sprintf('http://%s:%s/api-tests/stop-coverage', SWOOLE_TESTING_HOST, SWOOLE_TESTING_PORT), + sprintf('http://%s:%s/api-tests/stop-coverage', API_TESTS_HOST, API_TESTS_PORT), ); }); diff --git a/config/test/constants.php b/config/test/constants.php index a2c880fc..c767abc9 100644 --- a/config/test/constants.php +++ b/config/test/constants.php @@ -4,5 +4,5 @@ declare(strict_types=1); namespace ShlinkioTest\Shlink; -const SWOOLE_TESTING_HOST = '127.0.0.1'; -const SWOOLE_TESTING_PORT = 9999; +const API_TESTS_HOST = '127.0.0.1'; +const API_TESTS_PORT = 9999; diff --git a/config/test/test_config.global.php b/config/test/test_config.global.php index 8998dd22..9b338d7a 100644 --- a/config/test/test_config.global.php +++ b/config/test/test_config.global.php @@ -34,8 +34,8 @@ use function Shlinkio\Shlink\Config\env; use function sprintf; use function sys_get_temp_dir; -use const ShlinkioTest\Shlink\SWOOLE_TESTING_HOST; -use const ShlinkioTest\Shlink\SWOOLE_TESTING_PORT; +use const ShlinkioTest\Shlink\API_TESTS_HOST; +use const ShlinkioTest\Shlink\API_TESTS_PORT; $isApiTest = env('TEST_ENV') === 'api'; $isCliTest = env('TEST_ENV') === 'cli'; @@ -136,8 +136,8 @@ return [ 'mezzio-swoole' => [ 'enable_coroutine' => false, 'swoole-http-server' => [ - 'host' => SWOOLE_TESTING_HOST, - 'port' => SWOOLE_TESTING_PORT, + 'host' => API_TESTS_HOST, + 'port' => API_TESTS_PORT, 'process-name' => 'shlink_test', 'options' => [ 'pid_file' => sys_get_temp_dir() . '/shlink-test-swoole.pid', @@ -188,7 +188,7 @@ return [ 'dependencies' => [ 'services' => [ 'shlink_test_api_client' => new Client([ - 'base_uri' => sprintf('http://%s:%s/', SWOOLE_TESTING_HOST, SWOOLE_TESTING_PORT), + 'base_uri' => sprintf('http://%s:%s/', API_TESTS_HOST, API_TESTS_PORT), 'http_errors' => false, ]), ], diff --git a/indocker b/indocker index 789386ac..3dde53c4 100755 --- a/indocker +++ b/indocker @@ -1,8 +1,13 @@ #!/usr/bin/env bash # Run docker containers if they are not up yet -if ! [[ $(docker ps | grep shlink_swoole) ]]; then +if ! [[ $(docker ps | grep shlink) ]]; then docker-compose up -d fi -docker exec -it shlink_swoole /bin/sh -c "$*" +if [[ "$*" == *"test:api:rr"* ]]; then + # API tests should be run inside the RoadRunner container when the test runtime is RoadRunner + docker exec -it shlink_roadrunner /bin/sh -c "$*" +else + docker exec -it shlink_swoole /bin/sh -c "$*" +fi From 679bb8d357cfa99af2f743c08c6faa0eb8a45e27 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 27 Aug 2022 09:15:58 +0200 Subject: [PATCH 050/108] Added API tests over roadrunner on CI --- .github/actions/ci-setup/action.yml | 2 +- .github/workflows/ci.yml | 18 +++++++++++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/.github/actions/ci-setup/action.yml b/.github/actions/ci-setup/action.yml index eb7c8979..78cbdf1c 100644 --- a/.github/actions/ci-setup/action.yml +++ b/.github/actions/ci-setup/action.yml @@ -11,7 +11,7 @@ inputs: required: true php-extensions: description: 'The PHP extensions to install' - required: true + required: false default: '' extensions-cache-key: description: 'The key used to cache PHP extensions. If empty value is provided, extension caching is disabled' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 57985e8a..1fcebea2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,11 +29,27 @@ jobs: with: test-group: unit - api-tests: + api-tests-openswoole: uses: './.github/workflows/ci-tests.yml' with: test-group: api + api-tests-roadrunner: + runs-on: ubuntu-22.04 + strategy: + matrix: + php-version: [ '8.1' ] + steps: + - uses: actions/checkout@v3 + - run: docker-compose -f docker-compose.yml -f docker-compose.ci.yml up -d shlink_db_postgres + - uses: shivammathur/setup-php@v2 + with: + php-version: ${{ inputs.php-version }} + tools: composer + - run: composer install --no-interaction --prefer-dist + - run: ./vendor/bin/rr get --location bin/ && chmod +x bin/rr + - run: composer test:api:rr + cli-tests: uses: './.github/workflows/ci-tests.yml' with: From d54a2bde0fd3f11ef5a5b9890526a4ce3e0b2935 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 27 Aug 2022 09:18:46 +0200 Subject: [PATCH 051/108] Fixed reference to unknown job in CI workflow --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1fcebea2..804514db 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -96,7 +96,7 @@ jobs: api-mutation-tests: needs: - - api-tests + - api-tests-openswoole uses: './.github/workflows/ci-mutation-tests.yml' with: test-group: api From b7f7288a4b3a8fe3bff26753e941652469c8b0d5 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 27 Aug 2022 09:19:55 +0200 Subject: [PATCH 052/108] Fixed reference to unknown job in CI workflow --- .github/workflows/ci.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 804514db..60d3da2d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,12 +29,12 @@ jobs: with: test-group: unit - api-tests-openswoole: + openswoole-api-tests: uses: './.github/workflows/ci-tests.yml' with: test-group: api - api-tests-roadrunner: + roadrunner-api-tests: runs-on: ubuntu-22.04 strategy: matrix: @@ -96,7 +96,7 @@ jobs: api-mutation-tests: needs: - - api-tests-openswoole + - openswoole-api-tests uses: './.github/workflows/ci-mutation-tests.yml' with: test-group: api @@ -111,7 +111,7 @@ jobs: upload-coverage: needs: - unit-tests - - api-tests + - openswoole-api-tests - cli-tests - sqlite-db-tests runs-on: ubuntu-22.04 From aca5804f989c938f625218bb486d9341530b5c49 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 27 Aug 2022 09:21:17 +0200 Subject: [PATCH 053/108] Fixed usage of inputs instead of matrix on CI workflow --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 60d3da2d..709c9834 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,7 +44,7 @@ jobs: - run: docker-compose -f docker-compose.yml -f docker-compose.ci.yml up -d shlink_db_postgres - uses: shivammathur/setup-php@v2 with: - php-version: ${{ inputs.php-version }} + php-version: ${{ matrix.php-version }} tools: composer - run: composer install --no-interaction --prefer-dist - run: ./vendor/bin/rr get --location bin/ && chmod +x bin/rr From df70810aa64f2efed18e63c4f43158b3878750de Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 27 Aug 2022 09:23:45 +0200 Subject: [PATCH 054/108] Ensured tests are run in bash in CI --- .github/workflows/ci-tests.yml | 1 + .github/workflows/ci.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml index b7f7af98..3f3915c3 100644 --- a/.github/workflows/ci-tests.yml +++ b/.github/workflows/ci-tests.yml @@ -28,6 +28,7 @@ jobs: php-extensions: openswoole-4.11.1 extensions-cache-key: tests-extensions-${{ matrix.php-version }}-${{ inputs.test-group }} - run: composer test:${{ inputs.test-group }}:ci + shell: bash - uses: actions/upload-artifact@v3 if: ${{ matrix.php-version == '8.1' }} with: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 709c9834..857dbf59 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,6 +49,7 @@ jobs: - run: composer install --no-interaction --prefer-dist - run: ./vendor/bin/rr get --location bin/ && chmod +x bin/rr - run: composer test:api:rr + shell: bash cli-tests: uses: './.github/workflows/ci-tests.yml' From bfbeb7b1fb26515c88152a1a3b41d17b6e9395de Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 27 Aug 2022 09:36:37 +0200 Subject: [PATCH 055/108] Improved run-api-tests.sh script to make it compatible with sh --- .github/workflows/ci-tests.yml | 1 - .github/workflows/ci.yml | 1 - bin/test/run-api-tests.sh | 35 +++++++++++++++++++++------------- config/config.php | 3 ++- indocker | 7 +------ 5 files changed, 25 insertions(+), 22 deletions(-) diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml index 3f3915c3..b7f7af98 100644 --- a/.github/workflows/ci-tests.yml +++ b/.github/workflows/ci-tests.yml @@ -28,7 +28,6 @@ jobs: php-extensions: openswoole-4.11.1 extensions-cache-key: tests-extensions-${{ matrix.php-version }}-${{ inputs.test-group }} - run: composer test:${{ inputs.test-group }}:ci - shell: bash - uses: actions/upload-artifact@v3 if: ${{ matrix.php-version == '8.1' }} with: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 857dbf59..709c9834 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,7 +49,6 @@ jobs: - run: composer install --no-interaction --prefer-dist - run: ./vendor/bin/rr get --location bin/ && chmod +x bin/rr - run: composer test:api:rr - shell: bash cli-tests: uses: './.github/workflows/ci-tests.yml' diff --git a/bin/test/run-api-tests.sh b/bin/test/run-api-tests.sh index 1f61b681..24652cb7 100755 --- a/bin/test/run-api-tests.sh +++ b/bin/test/run-api-tests.sh @@ -12,26 +12,35 @@ mkdir data/log/api-tests touch $OUTPUT_LOGS # Try to stop server just in case it hanged in last execution -[[ $TEST_RUNTIME == 'openswoole' ]] && vendor/bin/laminas mezzio:swoole:stop -[[ $TEST_RUNTIME == 'rr' ]] && bin/rr stop -f +if test "$TEST_RUNTIME" == 'openswoole'; then + vendor/bin/laminas mezzio:swoole:stop +elif test "$TEST_RUNTIME" == 'rr'; then + bin/rr stop -f +fi echo 'Starting server...' -[[ $TEST_RUNTIME == 'openswoole' ]] && vendor/bin/laminas mezzio:swoole:start -d -[[ $TEST_RUNTIME == 'rr' ]] && bin/rr serve -p -c=config/roadrunner/.rr.dev.yml \ - -o=http.address=0.0.0.0:9999 \ - -o=logs.encoding=json \ - -o=logs.channels.http.encoding=json \ - -o=logs.channels.server.encoding=json \ - -o=logs.output="${PWD}/${OUTPUT_LOGS}" \ - -o=logs.channels.http.output="${PWD}/${OUTPUT_LOGS}" \ - -o=logs.channels.server.output="${PWD}/${OUTPUT_LOGS}" & +if test "$TEST_RUNTIME" == 'openswoole'; then + vendor/bin/laminas mezzio:swoole:start -d +elif test "$TEST_RUNTIME" == 'rr'; then + bin/rr serve -p -c=config/roadrunner/.rr.dev.yml \ + -o=http.address=0.0.0.0:9999 \ + -o=logs.encoding=json \ + -o=logs.channels.http.encoding=json \ + -o=logs.channels.server.encoding=json \ + -o=logs.output="${PWD}/${OUTPUT_LOGS}" \ + -o=logs.channels.http.output="${PWD}/${OUTPUT_LOGS}" \ + -o=logs.channels.server.output="${PWD}/${OUTPUT_LOGS}" & +fi sleep 2 # Let's give the server a couple of seconds to start vendor/bin/phpunit --order-by=random -c phpunit-api.xml --testdox --colors=always --log-junit=build/coverage-api/junit.xml $* testsExitCode=$? -[[ $TEST_RUNTIME == 'openswoole' ]] && vendor/bin/laminas mezzio:swoole:stop -[[ $TEST_RUNTIME == 'rr' ]] && bin/rr stop -c config/roadrunner/.rr.dev.yml -o=http.address=0.0.0.0:9999 +if test "$TEST_RUNTIME" == 'openswoole'; then + vendor/bin/laminas mezzio:swoole:stop +elif test "$TEST_RUNTIME" == 'rr'; then + bin/rr stop -c config/roadrunner/.rr.dev.yml -o=http.address=0.0.0.0:9999 +fi # Exit this script with the same code as the tests. If tests failed, this script has to fail exit $testsExitCode diff --git a/config/config.php b/config/config.php index 2763d23d..cacf9bcd 100644 --- a/config/config.php +++ b/config/config.php @@ -17,8 +17,9 @@ use function Shlinkio\Shlink\Config\swooleIsInstalled; use const PHP_SAPI; -$enableSwoole = PHP_SAPI === 'cli' && swooleIsInstalled(); $isTestEnv = env('APP_ENV') === 'test'; +$isRR = env('RR_MODE') !== null; +$enableSwoole = PHP_SAPI === 'cli' && ! $isRR && swooleIsInstalled(); return (new ConfigAggregator\ConfigAggregator([ ! $isTestEnv diff --git a/indocker b/indocker index 3dde53c4..03061e2f 100755 --- a/indocker +++ b/indocker @@ -5,9 +5,4 @@ if ! [[ $(docker ps | grep shlink) ]]; then docker-compose up -d fi -if [[ "$*" == *"test:api:rr"* ]]; then - # API tests should be run inside the RoadRunner container when the test runtime is RoadRunner - docker exec -it shlink_roadrunner /bin/sh -c "$*" -else - docker exec -it shlink_swoole /bin/sh -c "$*" -fi +docker exec -it shlink_swoole /bin/sh -c "$*" From 8260a0843ba9346b0efb7fd96e42178780e7f863 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 27 Aug 2022 09:43:20 +0200 Subject: [PATCH 056/108] Undone changes for sh on API tests --- .github/workflows/ci-tests.yml | 1 + .github/workflows/ci.yml | 1 + bin/test/run-api-tests.sh | 38 +++++++++++++--------------------- 3 files changed, 16 insertions(+), 24 deletions(-) diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml index b7f7af98..3f3915c3 100644 --- a/.github/workflows/ci-tests.yml +++ b/.github/workflows/ci-tests.yml @@ -28,6 +28,7 @@ jobs: php-extensions: openswoole-4.11.1 extensions-cache-key: tests-extensions-${{ matrix.php-version }}-${{ inputs.test-group }} - run: composer test:${{ inputs.test-group }}:ci + shell: bash - uses: actions/upload-artifact@v3 if: ${{ matrix.php-version == '8.1' }} with: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 709c9834..857dbf59 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,6 +49,7 @@ jobs: - run: composer install --no-interaction --prefer-dist - run: ./vendor/bin/rr get --location bin/ && chmod +x bin/rr - run: composer test:api:rr + shell: bash cli-tests: uses: './.github/workflows/ci-tests.yml' diff --git a/bin/test/run-api-tests.sh b/bin/test/run-api-tests.sh index 24652cb7..c115feac 100755 --- a/bin/test/run-api-tests.sh +++ b/bin/test/run-api-tests.sh @@ -1,8 +1,7 @@ -#!/usr/bin/env sh export APP_ENV=test -export DB_DRIVER=postgres export TEST_ENV=api export TEST_RUNTIME=${TEST_RUNTIME:-"openswoole"} +export DB_DRIVER=${DB_DRIVER:-"postgres"} export GENERATE_COVERAGE=${GENERATE_COVERAGE:-"no"} # Reset logs @@ -12,35 +11,26 @@ mkdir data/log/api-tests touch $OUTPUT_LOGS # Try to stop server just in case it hanged in last execution -if test "$TEST_RUNTIME" == 'openswoole'; then - vendor/bin/laminas mezzio:swoole:stop -elif test "$TEST_RUNTIME" == 'rr'; then - bin/rr stop -f -fi +[[ $TEST_RUNTIME == 'openswoole' ]] && vendor/bin/laminas mezzio:swoole:stop +[[ $TEST_RUNTIME == 'rr' ]] && bin/rr stop -f echo 'Starting server...' -if test "$TEST_RUNTIME" == 'openswoole'; then - vendor/bin/laminas mezzio:swoole:start -d -elif test "$TEST_RUNTIME" == 'rr'; then - bin/rr serve -p -c=config/roadrunner/.rr.dev.yml \ - -o=http.address=0.0.0.0:9999 \ - -o=logs.encoding=json \ - -o=logs.channels.http.encoding=json \ - -o=logs.channels.server.encoding=json \ - -o=logs.output="${PWD}/${OUTPUT_LOGS}" \ - -o=logs.channels.http.output="${PWD}/${OUTPUT_LOGS}" \ - -o=logs.channels.server.output="${PWD}/${OUTPUT_LOGS}" & -fi +[[ $TEST_RUNTIME == 'openswoole' ]] && vendor/bin/laminas mezzio:swoole:start -d +[[ $TEST_RUNTIME == 'rr' ]] && bin/rr serve -p -c=config/roadrunner/.rr.dev.yml \ + -o=http.address=0.0.0.0:9999 \ + -o=logs.encoding=json \ + -o=logs.channels.http.encoding=json \ + -o=logs.channels.server.encoding=json \ + -o=logs.output="${PWD}/${OUTPUT_LOGS}" \ + -o=logs.channels.http.output="${PWD}/${OUTPUT_LOGS}" \ + -o=logs.channels.server.output="${PWD}/${OUTPUT_LOGS}" & sleep 2 # Let's give the server a couple of seconds to start vendor/bin/phpunit --order-by=random -c phpunit-api.xml --testdox --colors=always --log-junit=build/coverage-api/junit.xml $* testsExitCode=$? -if test "$TEST_RUNTIME" == 'openswoole'; then - vendor/bin/laminas mezzio:swoole:stop -elif test "$TEST_RUNTIME" == 'rr'; then - bin/rr stop -c config/roadrunner/.rr.dev.yml -o=http.address=0.0.0.0:9999 -fi +[[ $TEST_RUNTIME == 'openswoole' ]] && vendor/bin/laminas mezzio:swoole:stop +[[ $TEST_RUNTIME == 'rr' ]] && bin/rr stop -c config/roadrunner/.rr.dev.yml -o=http.address=0.0.0.0:9999 # Exit this script with the same code as the tests. If tests failed, this script has to fail exit $testsExitCode From f71c95b74a747a863209c796a4143e38ae0fb7f4 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 27 Aug 2022 09:49:32 +0200 Subject: [PATCH 057/108] Another attempt to make API tests script sh compatible --- bin/test/run-api-tests.sh | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/bin/test/run-api-tests.sh b/bin/test/run-api-tests.sh index c115feac..1cbf948a 100755 --- a/bin/test/run-api-tests.sh +++ b/bin/test/run-api-tests.sh @@ -1,8 +1,10 @@ +#!/usr/bin/env sh + export APP_ENV=test export TEST_ENV=api -export TEST_RUNTIME=${TEST_RUNTIME:-"openswoole"} -export DB_DRIVER=${DB_DRIVER:-"postgres"} -export GENERATE_COVERAGE=${GENERATE_COVERAGE:-"no"} +export TEST_RUNTIME="${TEST_RUNTIME:-"openswoole"}" +export DB_DRIVER="${DB_DRIVER:-"postgres"}" +export GENERATE_COVERAGE="${GENERATE_COVERAGE:-"no"}" # Reset logs OUTPUT_LOGS=data/log/api-tests/output.log @@ -11,12 +13,12 @@ mkdir data/log/api-tests touch $OUTPUT_LOGS # Try to stop server just in case it hanged in last execution -[[ $TEST_RUNTIME == 'openswoole' ]] && vendor/bin/laminas mezzio:swoole:stop -[[ $TEST_RUNTIME == 'rr' ]] && bin/rr stop -f +[ "$TEST_RUNTIME" = 'openswoole' ] && vendor/bin/laminas mezzio:swoole:stop +[ "$TEST_RUNTIME" = 'rr' ] && bin/rr stop -f echo 'Starting server...' -[[ $TEST_RUNTIME == 'openswoole' ]] && vendor/bin/laminas mezzio:swoole:start -d -[[ $TEST_RUNTIME == 'rr' ]] && bin/rr serve -p -c=config/roadrunner/.rr.dev.yml \ +[ "$TEST_RUNTIME" = 'openswoole' ] && vendor/bin/laminas mezzio:swoole:start -d +[ "$TEST_RUNTIME" = 'rr' ] && bin/rr serve -p -c=config/roadrunner/.rr.dev.yml \ -o=http.address=0.0.0.0:9999 \ -o=logs.encoding=json \ -o=logs.channels.http.encoding=json \ @@ -29,8 +31,8 @@ sleep 2 # Let's give the server a couple of seconds to start vendor/bin/phpunit --order-by=random -c phpunit-api.xml --testdox --colors=always --log-junit=build/coverage-api/junit.xml $* testsExitCode=$? -[[ $TEST_RUNTIME == 'openswoole' ]] && vendor/bin/laminas mezzio:swoole:stop -[[ $TEST_RUNTIME == 'rr' ]] && bin/rr stop -c config/roadrunner/.rr.dev.yml -o=http.address=0.0.0.0:9999 +[ "$TEST_RUNTIME" = 'openswoole' ] && vendor/bin/laminas mezzio:swoole:stop +[ "$TEST_RUNTIME" = 'rr' ] && bin/rr stop -c config/roadrunner/.rr.dev.yml -o=http.address=0.0.0.0:9999 # Exit this script with the same code as the tests. If tests failed, this script has to fail exit $testsExitCode From 529ddacafe71524163343764fcf52088595647f8 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 27 Aug 2022 09:54:15 +0200 Subject: [PATCH 058/108] Removed usage of bash again from tests in CI, as it does nothing really --- .github/workflows/ci-tests.yml | 1 - .github/workflows/ci.yml | 1 - 2 files changed, 2 deletions(-) diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml index 3f3915c3..b7f7af98 100644 --- a/.github/workflows/ci-tests.yml +++ b/.github/workflows/ci-tests.yml @@ -28,7 +28,6 @@ jobs: php-extensions: openswoole-4.11.1 extensions-cache-key: tests-extensions-${{ matrix.php-version }}-${{ inputs.test-group }} - run: composer test:${{ inputs.test-group }}:ci - shell: bash - uses: actions/upload-artifact@v3 if: ${{ matrix.php-version == '8.1' }} with: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 857dbf59..709c9834 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,7 +49,6 @@ jobs: - run: composer install --no-interaction --prefer-dist - run: ./vendor/bin/rr get --location bin/ && chmod +x bin/rr - run: composer test:api:rr - shell: bash cli-tests: uses: './.github/workflows/ci-tests.yml' From a739eb6d60146d83bcec3bf1609597441ed6647e Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 27 Aug 2022 13:01:38 +0200 Subject: [PATCH 059/108] Added support to build the docker image with RoadRunner instead of openswoole --- .dockerignore | 2 +- Dockerfile | 22 +++++++++++++++----- composer.json | 2 +- config/autoload/url-shortener.local.php.dist | 10 ++++----- config/config.php | 7 ++++--- config/roadrunner/.rr.dev.yml | 4 ++-- docker/config/shlink_in_docker.local.php | 3 +++ docker/docker-entrypoint.sh | 10 ++++++--- 8 files changed, 40 insertions(+), 20 deletions(-) diff --git a/.dockerignore b/.dockerignore index ac530ed3..beca6373 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,3 +1,4 @@ +bin/rr config/autoload/*local* data/infra data/cache/* @@ -23,4 +24,3 @@ infection* build* **/.* !config/roadrunner/.rr.yml -bin/helper diff --git a/Dockerfile b/Dockerfile index 2944db45..bc0f4fe2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,6 +2,8 @@ FROM php:8.1.9-alpine3.16 as base ARG SHLINK_VERSION=latest ENV SHLINK_VERSION ${SHLINK_VERSION} +ARG SHLINK_RUNTIME=openswoole +ENV SHLINK_RUNTIME ${SHLINK_RUNTIME} ENV OPENSWOOLE_VERSION 4.11.1 ENV PDO_SQLSRV_VERSION 5.10.1 ENV MS_ODBC_SQL_VERSION 17.5.2.2 @@ -22,8 +24,10 @@ RUN \ # Install openswoole and sqlsrv driver for x86_64 builds RUN apk add --no-cache --virtual .phpize-deps ${PHPIZE_DEPS} unixodbc-dev && \ - pecl install openswoole-${OPENSWOOLE_VERSION} && \ - docker-php-ext-enable openswoole && \ + if [ "$SHLINK_RUNTIME" == 'openswoole' ]; then \ + pecl install openswoole-${OPENSWOOLE_VERSION} && \ + docker-php-ext-enable openswoole ; \ + fi; \ if [ $(uname -m) == "x86_64" ]; then \ wget https://download.microsoft.com/download/e/4/e/e4e67866-dffd-428c-aac7-8d28ddafb39b/msodbcsql17_${MS_ODBC_SQL_VERSION}-1_amd64.apk && \ apk add --no-cache --allow-untrusted msodbcsql17_${MS_ODBC_SQL_VERSION}-1_amd64.apk && \ @@ -38,7 +42,12 @@ FROM base as builder COPY . . COPY --from=composer:2 /usr/bin/composer ./composer.phar RUN apk add --no-cache git && \ - php composer.phar install --no-dev --optimize-autoloader --prefer-dist --no-progress --no-interaction && \ + php composer.phar install --no-dev --prefer-dist --optimize-autoloader --no-progress --no-interaction && \ + if [ "$SHLINK_RUNTIME" == 'openswoole' ]; then \ + php composer.phar remove spiral/roadrunner spiral/roadrunner-jobs --with-all-dependencies --update-no-dev --optimize-autoloader --no-progress --no-interactionc ; \ + elif [ $SHLINK_RUNTIME == 'rr' ]; then \ + php composer.phar remove mezzio/mezzio-swoole --with-all-dependencies --update-no-dev --optimize-autoloader --no-progress --no-interaction ; \ + fi; \ php composer.phar clear-cache && \ rm -r docker composer.* && \ sed -i "s/%SHLINK_VERSION%/${SHLINK_VERSION}/g" config/autoload/app_options.global.php @@ -49,9 +58,12 @@ FROM base LABEL maintainer="Alejandro Celaya " COPY --from=builder /etc/shlink . -RUN ln -s /etc/shlink/bin/cli /usr/local/bin/shlink +RUN ln -s /etc/shlink/bin/cli /usr/local/bin/shlink && \ + if [ "$SHLINK_RUNTIME" == 'rr' ]; then \ + php ./vendor/bin/rr get --location bin/ && chmod +x bin/rr ; \ + fi; -# Expose default openswoole port +# Expose default port EXPOSE 8080 # Copy config specific for the image diff --git a/composer.json b/composer.json index 54d180e3..d8b2874f 100644 --- a/composer.json +++ b/composer.json @@ -44,7 +44,7 @@ "pugx/shortid-php": "^1.0", "ramsey/uuid": "^4.3", "shlinkio/shlink-common": "^5.0", - "shlinkio/shlink-config": "dev-main#24ccd64 as 2.1", + "shlinkio/shlink-config": "dev-main#33004e6 as 2.1", "shlinkio/shlink-event-dispatcher": "dev-main#48c0137 as 2.6", "shlinkio/shlink-importer": "^4.0", "shlinkio/shlink-installer": "^8.1", diff --git a/config/autoload/url-shortener.local.php.dist b/config/autoload/url-shortener.local.php.dist index 1ed5a47e..f49570e1 100644 --- a/config/autoload/url-shortener.local.php.dist +++ b/config/autoload/url-shortener.local.php.dist @@ -2,8 +2,8 @@ declare(strict_types=1); -use function Shlinkio\Shlink\Config\env; -use function Shlinkio\Shlink\Config\swooleIsInstalled; +use function Shlinkio\Shlink\Config\runningInOpenswoole; +use function Shlinkio\Shlink\Config\runningInRoadRunner; return [ @@ -11,9 +11,9 @@ return [ 'domain' => [ 'schema' => 'http', 'hostname' => sprintf('localhost:%s', match (true) { - PHP_SAPI === 'cli' && env('RR_MODE') !== null => '8800', // Roadrunner - swooleIsInstalled() => '8080', // Swoole - default => '8000', // FPM + runningInRoadRunner() => '8800', + runningInOpenswoole() => '8080', + default => '8000', }), ], 'auto_resolve_titles' => true, diff --git a/config/config.php b/config/config.php index cacf9bcd..1c4fa40b 100644 --- a/config/config.php +++ b/config/config.php @@ -13,13 +13,14 @@ use Shlinkio\Shlink\Config\ConfigAggregator\EnvVarLoaderProvider; use function class_exists; use function Shlinkio\Shlink\Config\env; -use function Shlinkio\Shlink\Config\swooleIsInstalled; +use function Shlinkio\Shlink\Config\openswooleIsInstalled; +use function Shlinkio\Shlink\Config\runningInRoadRunner; use const PHP_SAPI; $isTestEnv = env('APP_ENV') === 'test'; -$isRR = env('RR_MODE') !== null; -$enableSwoole = PHP_SAPI === 'cli' && ! $isRR && swooleIsInstalled(); +// TODO Could check for actual openswoole with runningInOpenswoole()?? +$enableSwoole = PHP_SAPI === 'cli' && ! runningInRoadRunner() && openswooleIsInstalled(); return (new ConfigAggregator\ConfigAggregator([ ! $isTestEnv diff --git a/config/roadrunner/.rr.dev.yml b/config/roadrunner/.rr.dev.yml index 7f06fc9f..df6e2993 100644 --- a/config/roadrunner/.rr.dev.yml +++ b/config/roadrunner/.rr.dev.yml @@ -46,8 +46,8 @@ reload: patterns: ['.php'] services: http: - dirs: ['../../bin', '../../config', '../../data/migrations', '../../docker', '../../module', '../../vendor'] + dirs: ['../../bin', '../../config', '../../data/migrations', '../../module', '../../vendor'] recursive: true jobs: - dirs: ['../../bin', '../../config', '../../data/migrations', '../../docker', '../../module', '../../vendor'] + dirs: ['../../bin', '../../config', '../../data/migrations', '../../module', '../../vendor'] recursive: true diff --git a/docker/config/shlink_in_docker.local.php b/docker/config/shlink_in_docker.local.php index 4fba24b6..9dc99351 100644 --- a/docker/config/shlink_in_docker.local.php +++ b/docker/config/shlink_in_docker.local.php @@ -6,11 +6,14 @@ namespace Shlinkio\Shlink; use Shlinkio\Shlink\Common\Logger\LoggerType; +use function Shlinkio\Shlink\Config\runningInRoadRunner; + return [ 'logger' => [ 'Shlink' => [ 'type' => LoggerType::STREAM->value, + 'destination' => runningInRoadRunner() ? 'php://stderr' : 'php://stdout', ], ], diff --git a/docker/docker-entrypoint.sh b/docker/docker-entrypoint.sh index f1c4c495..c191afe7 100644 --- a/docker/docker-entrypoint.sh +++ b/docker/docker-entrypoint.sh @@ -31,6 +31,10 @@ if [ $ENABLE_PERIODIC_VISIT_LOCATE ]; then /usr/sbin/crond & fi -# When restarting the container, openswoole might think it is already in execution -# This forces the app to be started every second until the exit code is 0 -until php vendor/bin/laminas mezzio:swoole:start; do sleep 1 ; done +if [ "$SHLINK_RUNTIME" == 'openswoole' ]; then + # When restarting the container, openswoole might think it is already in execution + # This forces the app to be started every second until the exit code is 0 + until php vendor/bin/laminas mezzio:swoole:start; do sleep 1 ; done +elif [ "$SHLINK_RUNTIME" == 'rr' ]; then + ./bin/rr serve -c config/roadrunner/.rr.yml +fi From 2099ea16ec853a99c7cdc1239d3d99ec5e5a9c3f Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 27 Aug 2022 13:14:27 +0200 Subject: [PATCH 060/108] Added stage to build docker images for roadrunner --- .github/workflows/docker-image-build.yml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/docker-image-build.yml b/.github/workflows/docker-image-build.yml index cbd8b213..11396f3f 100644 --- a/.github/workflows/docker-image-build.yml +++ b/.github/workflows/docker-image-build.yml @@ -8,9 +8,19 @@ on: - 'v*' jobs: - build: + build-openswool: uses: shlinkio/github-actions/.github/workflows/docker-build-and-publish.yml@main secrets: inherit with: image-name: shlinkio/shlink version-arg-name: SHLINK_VERSION + + build-roadrunner: + uses: shlinkio/github-actions/.github/workflows/docker-build-and-publish.yml@main + secrets: inherit + with: + image-name: shlinkio/shlink + version-arg-name: SHLINK_VERSION + tags-suffix: roadrunner + extra-build-args: | + SHLINK_RUNTIME=rr From 885273911154444fc55988cb50b6865157dda99d Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 27 Aug 2022 17:17:37 +0200 Subject: [PATCH 061/108] Ensured some dynamic RR config for prod env, based on env vars --- .github/workflows/ci.yml | 2 +- Dockerfile | 2 +- config/roadrunner/.rr.dev.yml | 13 ++++--------- config/roadrunner/.rr.yml | 17 ++++++----------- data/infra/roadrunner.Dockerfile | 2 +- docker/docker-entrypoint.sh | 5 +++++ 6 files changed, 18 insertions(+), 23 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 709c9834..354c4752 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,7 +47,7 @@ jobs: php-version: ${{ matrix.php-version }} tools: composer - run: composer install --no-interaction --prefer-dist - - run: ./vendor/bin/rr get --location bin/ && chmod +x bin/rr + - run: ./vendor/bin/rr get --no-interaction --location bin/ && chmod +x bin/rr - run: composer test:api:rr cli-tests: diff --git a/Dockerfile b/Dockerfile index bc0f4fe2..2835d75f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -60,7 +60,7 @@ LABEL maintainer="Alejandro Celaya " COPY --from=builder /etc/shlink . RUN ln -s /etc/shlink/bin/cli /usr/local/bin/shlink && \ if [ "$SHLINK_RUNTIME" == 'rr' ]; then \ - php ./vendor/bin/rr get --location bin/ && chmod +x bin/rr ; \ + php ./vendor/bin/rr get --no-interaction --location bin/ && chmod +x bin/rr ; \ fi; # Expose default port diff --git a/config/roadrunner/.rr.dev.yml b/config/roadrunner/.rr.dev.yml index df6e2993..a12f2475 100644 --- a/config/roadrunner/.rr.dev.yml +++ b/config/roadrunner/.rr.dev.yml @@ -9,19 +9,14 @@ server: http: address: '0.0.0.0:8080' static: - dir: ../../public - forbid: - - .php - - .htaccess + dir: '../../public' + forbid: ['.php', '.htaccess'] pool: - num_workers: 3 - supervisor: - max_worker_memory: 100 + num_workers: 16 jobs: pool: - num_workers: 2 - max_worker_memory: 100 + num_workers: 16 timeout: 300 consume: ['shlink'] pipelines: diff --git a/config/roadrunner/.rr.yml b/config/roadrunner/.rr.yml index 032989b9..f7409c48 100644 --- a/config/roadrunner/.rr.yml +++ b/config/roadrunner/.rr.yml @@ -7,22 +7,17 @@ server: command: 'php -dopcache.enable_cli=1 -dopcache.validate_timestamps=0 ../../bin/roadrunner-worker.php' http: - address: '0.0.0.0:8080' + address: '0.0.0.0:${PORT}' static: - dir: ../../public - forbid: - - .php - - .htaccess + dir: '../../public' + forbid: ['.php', '.htaccess'] pool: - num_workers: 16 # TODO Make configurable - supervisor: - max_worker_memory: 100 + num_workers: ${WEB_WORKER_NUM} jobs: - timeout: 300 + timeout: 300 # 5 minutes pool: - num_workers: 16 # TODO Make configurable - max_worker_memory: 100 + num_workers: ${TASK_WORKER_NUM} consume: ['shlink'] pipelines: shlink: diff --git a/data/infra/roadrunner.Dockerfile b/data/infra/roadrunner.Dockerfile index 7a68a7ae..8520b92d 100644 --- a/data/infra/roadrunner.Dockerfile +++ b/data/infra/roadrunner.Dockerfile @@ -68,6 +68,6 @@ CMD \ # Install dependencies if the vendor dir does not exist if [[ ! -d "./vendor" ]]; then /usr/local/bin/composer install ; fi && \ # Download roadrunner binary - if [[ ! -f "./bin/rr" ]]; then ./vendor/bin/rr get --location bin/ && chmod +x bin/rr ; fi && \ + if [[ ! -f "./bin/rr" ]]; then ./vendor/bin/rr get --no-interaction --location bin/ && chmod +x bin/rr ; fi && \ # This forces the app to be started every second until the exit code is 0 until ./bin/rr serve -c config/roadrunner/.rr.dev.yml; do sleep 1 ; done diff --git a/docker/docker-entrypoint.sh b/docker/docker-entrypoint.sh index c191afe7..a1cc03be 100644 --- a/docker/docker-entrypoint.sh +++ b/docker/docker-entrypoint.sh @@ -31,6 +31,11 @@ if [ $ENABLE_PERIODIC_VISIT_LOCATE ]; then /usr/sbin/crond & fi +# RoadRunner config needs these to have been set, so falling back to default values if not set yet +export PORT="${PORT:-"8765"}" +export WEB_WORKER_NUM="${WEB_WORKER_NUM:-"16"}" +export TASK_WORKER_NUM="${TASK_WORKER_NUM:-"16"}" + if [ "$SHLINK_RUNTIME" == 'openswoole' ]; then # When restarting the container, openswoole might think it is already in execution # This forces the app to be started every second until the exit code is 0 From a4f979be080d563419e9df378babf5af2fdd961f Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 27 Aug 2022 17:27:54 +0200 Subject: [PATCH 062/108] Enabled support for static files from public dir via RoadRunner --- config/config.php | 7 ++----- config/roadrunner/.rr.dev.yml | 1 + config/roadrunner/.rr.yml | 1 + 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/config/config.php b/config/config.php index 1c4fa40b..088cad0a 100644 --- a/config/config.php +++ b/config/config.php @@ -13,14 +13,11 @@ use Shlinkio\Shlink\Config\ConfigAggregator\EnvVarLoaderProvider; use function class_exists; use function Shlinkio\Shlink\Config\env; -use function Shlinkio\Shlink\Config\openswooleIsInstalled; +use function Shlinkio\Shlink\Config\runningInOpenswoole; use function Shlinkio\Shlink\Config\runningInRoadRunner; -use const PHP_SAPI; - $isTestEnv = env('APP_ENV') === 'test'; -// TODO Could check for actual openswoole with runningInOpenswoole()?? -$enableSwoole = PHP_SAPI === 'cli' && ! runningInRoadRunner() && openswooleIsInstalled(); +$enableSwoole = ! runningInRoadRunner() && runningInOpenswoole(); return (new ConfigAggregator\ConfigAggregator([ ! $isTestEnv diff --git a/config/roadrunner/.rr.dev.yml b/config/roadrunner/.rr.dev.yml index a12f2475..7adf4520 100644 --- a/config/roadrunner/.rr.dev.yml +++ b/config/roadrunner/.rr.dev.yml @@ -8,6 +8,7 @@ server: http: address: '0.0.0.0:8080' + middleware: ['static'] static: dir: '../../public' forbid: ['.php', '.htaccess'] diff --git a/config/roadrunner/.rr.yml b/config/roadrunner/.rr.yml index f7409c48..40c93b2b 100644 --- a/config/roadrunner/.rr.yml +++ b/config/roadrunner/.rr.yml @@ -8,6 +8,7 @@ server: http: address: '0.0.0.0:${PORT}' + middleware: ['static'] static: dir: '../../public' forbid: ['.php', '.htaccess'] From 7b637d6a616d13a305369a3d2c95afa1506a21d5 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 27 Aug 2022 17:48:59 +0200 Subject: [PATCH 063/108] Ensured RoadRunner deps are removed when building openswoole dist file --- build.sh | 6 +++++- config/roadrunner/.rr.yml | 2 -- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/build.sh b/build.sh index e274210a..219ccd2d 100755 --- a/build.sh +++ b/build.sh @@ -24,18 +24,22 @@ rsync -av * "${builtContent}" \ --exclude=*docker* \ --exclude=Dockerfile \ --include=.htaccess \ + --include=config/roadrunner/.rr.yml \ --exclude-from=./.dockerignore cd "${builtContent}" # Install dependencies echo "Installing dependencies with $composerBin..." -composerFlags="--optimize-autoloader --no-progress --no-interaction" +composerFlags="--optimize-autoloader --no-progress --no-interaction --ignore-platform-reqs" ${composerBin} self-update ${composerBin} install --no-dev --prefer-dist $composerFlags if [[ $noSwoole ]]; then # If generating a dist not for openswoole, uninstall mezzio-swoole ${composerBin} remove mezzio/mezzio-swoole --with-all-dependencies --update-no-dev $composerFlags +else + # If generating a dist for openswoole, uninstall RoadRunner + ${composerBin} remove spiral/roadrunner spiral/roadrunner-jobs --with-all-dependencies --update-no-dev $composerFlags fi # Delete development files diff --git a/config/roadrunner/.rr.yml b/config/roadrunner/.rr.yml index 40c93b2b..d44801ee 100644 --- a/config/roadrunner/.rr.yml +++ b/config/roadrunner/.rr.yml @@ -34,5 +34,3 @@ logs: level: info # Log all http requests, set to info to disable server: level: debug # Everything written to worker stderr is logged - metrics: - level: error From 7daa60263097de1db4310b91ccfb42a27ed4f0ef Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 27 Aug 2022 17:51:14 +0200 Subject: [PATCH 064/108] Removed accidental flag in build script --- build.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sh b/build.sh index 219ccd2d..d9cda64d 100755 --- a/build.sh +++ b/build.sh @@ -30,7 +30,7 @@ cd "${builtContent}" # Install dependencies echo "Installing dependencies with $composerBin..." -composerFlags="--optimize-autoloader --no-progress --no-interaction --ignore-platform-reqs" +composerFlags="--optimize-autoloader --no-progress --no-interaction" ${composerBin} self-update ${composerBin} install --no-dev --prefer-dist $composerFlags From 2cf21ab3bd5cd828d197568a951184ec31d7a676 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 27 Aug 2022 19:38:05 +0200 Subject: [PATCH 065/108] Fixed openswoole E2E tests --- .github/workflows/ci.yml | 10 +++++----- config/config.php | 5 +++-- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 354c4752..f9d3660e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,6 +29,11 @@ jobs: with: test-group: unit + cli-tests: + uses: './.github/workflows/ci-tests.yml' + with: + test-group: cli + openswoole-api-tests: uses: './.github/workflows/ci-tests.yml' with: @@ -50,11 +55,6 @@ jobs: - run: ./vendor/bin/rr get --no-interaction --location bin/ && chmod +x bin/rr - run: composer test:api:rr - cli-tests: - uses: './.github/workflows/ci-tests.yml' - with: - test-group: cli - sqlite-db-tests: uses: './.github/workflows/ci-db-tests.yml' with: diff --git a/config/config.php b/config/config.php index 088cad0a..4f4a1359 100644 --- a/config/config.php +++ b/config/config.php @@ -13,11 +13,12 @@ use Shlinkio\Shlink\Config\ConfigAggregator\EnvVarLoaderProvider; use function class_exists; use function Shlinkio\Shlink\Config\env; -use function Shlinkio\Shlink\Config\runningInOpenswoole; use function Shlinkio\Shlink\Config\runningInRoadRunner; +use const PHP_SAPI; + $isTestEnv = env('APP_ENV') === 'test'; -$enableSwoole = ! runningInRoadRunner() && runningInOpenswoole(); +$enableSwoole = PHP_SAPI === 'cli' && ! runningInRoadRunner(); return (new ConfigAggregator\ConfigAggregator([ ! $isTestEnv From f97effcfe080471614400a440c8457b50697858d Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 27 Aug 2022 19:40:52 +0200 Subject: [PATCH 066/108] Fixed rr E2E tests --- config/config.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/config/config.php b/config/config.php index 4f4a1359..15a45348 100644 --- a/config/config.php +++ b/config/config.php @@ -13,12 +13,13 @@ use Shlinkio\Shlink\Config\ConfigAggregator\EnvVarLoaderProvider; use function class_exists; use function Shlinkio\Shlink\Config\env; +use function Shlinkio\Shlink\Config\openswooleIsInstalled; use function Shlinkio\Shlink\Config\runningInRoadRunner; use const PHP_SAPI; $isTestEnv = env('APP_ENV') === 'test'; -$enableSwoole = PHP_SAPI === 'cli' && ! runningInRoadRunner(); +$enableSwoole = PHP_SAPI === 'cli' && openswooleIsInstalled() && ! runningInRoadRunner(); return (new ConfigAggregator\ConfigAggregator([ ! $isTestEnv From 463d8e8950eb81cf22db71af0da296cae83fe242 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 27 Aug 2022 19:51:59 +0200 Subject: [PATCH 067/108] Updated changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 691af5de..6f4ef358 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this Non-error responses are not affected. * [#1513](https://github.com/shlinkio/shlink/issues/1513) Added publishing of the docker image in GHCR. +* [#1221](https://github.com/shlinkio/shlink/issues/1221) Added experimental support to run Shlink with [RoadRunner](https://roadrunner.dev) instead of openswoole. ### Changed * [#1339](https://github.com/shlinkio/shlink/issues/1339) Added new test suite for CLI E2E tests. From 2bca2606274a826e78e6af6b3499116f23e4b197 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 27 Aug 2022 21:49:20 +0200 Subject: [PATCH 068/108] What're RoadRunner docker images is not tried to build for arm/v7 --- .github/workflows/docker-image-build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/docker-image-build.yml b/.github/workflows/docker-image-build.yml index 11396f3f..96457033 100644 --- a/.github/workflows/docker-image-build.yml +++ b/.github/workflows/docker-image-build.yml @@ -21,6 +21,7 @@ jobs: with: image-name: shlinkio/shlink version-arg-name: SHLINK_VERSION + platforms: 'linux/arm64/v8,linux/amd64' tags-suffix: roadrunner extra-build-args: | SHLINK_RUNTIME=rr From 40e16703148d8263b10b04d7b8d24f35200df900 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 28 Aug 2022 10:18:02 +0200 Subject: [PATCH 069/108] Fixed default port in docker entry point --- docker/docker-entrypoint.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/docker-entrypoint.sh b/docker/docker-entrypoint.sh index a1cc03be..065afa2a 100644 --- a/docker/docker-entrypoint.sh +++ b/docker/docker-entrypoint.sh @@ -32,7 +32,7 @@ if [ $ENABLE_PERIODIC_VISIT_LOCATE ]; then fi # RoadRunner config needs these to have been set, so falling back to default values if not set yet -export PORT="${PORT:-"8765"}" +export PORT="${PORT:-"8080"}" export WEB_WORKER_NUM="${WEB_WORKER_NUM:-"16"}" export TASK_WORKER_NUM="${TASK_WORKER_NUM:-"16"}" From c53ba7b119a319dc53ef2099e20c5b34ca993722 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 3 Sep 2022 17:02:57 +0200 Subject: [PATCH 070/108] Exported defaults for env vars in docker only when the runtime is RoadRunner --- docker/docker-entrypoint.sh | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/docker/docker-entrypoint.sh b/docker/docker-entrypoint.sh index 065afa2a..1955f0ec 100644 --- a/docker/docker-entrypoint.sh +++ b/docker/docker-entrypoint.sh @@ -32,9 +32,12 @@ if [ $ENABLE_PERIODIC_VISIT_LOCATE ]; then fi # RoadRunner config needs these to have been set, so falling back to default values if not set yet -export PORT="${PORT:-"8080"}" -export WEB_WORKER_NUM="${WEB_WORKER_NUM:-"16"}" -export TASK_WORKER_NUM="${TASK_WORKER_NUM:-"16"}" +if [ "$SHLINK_RUNTIME" == 'rr' ]; then + export PORT="${PORT:-"8080"}" + # Default to 0 so that RoadRunner decides the number of workers based on the amount of logical CPUs + export WEB_WORKER_NUM="${WEB_WORKER_NUM:-"0"}" + export TASK_WORKER_NUM="${TASK_WORKER_NUM:-"0"}" +fi if [ "$SHLINK_RUNTIME" == 'openswoole' ]; then # When restarting the container, openswoole might think it is already in execution From fe4329d730d2cf6310ecf4665f3a906905e6538b Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Mon, 5 Sep 2022 20:56:49 +0200 Subject: [PATCH 071/108] Allowed trailing slashes support to be enabled for the short URLs route --- config/autoload/routes.config.php | 6 +++++- module/Core/src/Config/EnvVars.php | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/config/autoload/routes.config.php b/config/autoload/routes.config.php index 298b9349..b36c4b78 100644 --- a/config/autoload/routes.config.php +++ b/config/autoload/routes.config.php @@ -7,15 +7,19 @@ namespace Shlinkio\Shlink; use Fig\Http\Message\RequestMethodInterface; use RKA\Middleware\IpAddress; use Shlinkio\Shlink\Core\Action as CoreAction; +use Shlinkio\Shlink\Core\Config\EnvVars; use Shlinkio\Shlink\Rest\Action; use Shlinkio\Shlink\Rest\ConfigProvider; use Shlinkio\Shlink\Rest\Middleware; use Shlinkio\Shlink\Rest\Middleware\Mercure\NotConfiguredMercureErrorHandler; +use function sprintf; + return (static function (): array { $contentNegotiationMiddleware = Middleware\ShortUrl\CreateShortUrlContentNegotiationMiddleware::class; $dropDomainMiddleware = Middleware\ShortUrl\DropDefaultDomainFromRequestMiddleware::class; $overrideDomainMiddleware = Middleware\ShortUrl\OverrideDomainMiddleware::class; + $shortUrlRouteSuffix = EnvVars::SHORT_URL_TRAILING_SLASH->loadFromEnv(false) ? '[/]' : ''; return [ @@ -90,7 +94,7 @@ return (static function (): array { ], [ 'name' => CoreAction\RedirectAction::class, - 'path' => '/{shortCode}', + 'path' => sprintf('/{shortCode}%s', $shortUrlRouteSuffix), 'middleware' => [ IpAddress::class, CoreAction\RedirectAction::class, diff --git a/module/Core/src/Config/EnvVars.php b/module/Core/src/Config/EnvVars.php index ae93e4da..7cbd0af4 100644 --- a/module/Core/src/Config/EnvVars.php +++ b/module/Core/src/Config/EnvVars.php @@ -43,6 +43,7 @@ enum EnvVars: string case REDIRECT_STATUS_CODE = 'REDIRECT_STATUS_CODE'; case REDIRECT_CACHE_LIFETIME = 'REDIRECT_CACHE_LIFETIME'; case BASE_PATH = 'BASE_PATH'; + case SHORT_URL_TRAILING_SLASH = 'SHORT_URL_TRAILING_SLASH'; case PORT = 'PORT'; case TASK_WORKER_NUM = 'TASK_WORKER_NUM'; case WEB_WORKER_NUM = 'WEB_WORKER_NUM'; From c3c7ffad25bc63e6e2a7c57cbc5e4b2495528bba Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Mon, 5 Sep 2022 21:12:05 +0200 Subject: [PATCH 072/108] Updated to installer supporting trailing slash option --- composer.json | 2 +- config/autoload/installer.global.php | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index d8b2874f..99e6cb21 100644 --- a/composer.json +++ b/composer.json @@ -47,7 +47,7 @@ "shlinkio/shlink-config": "dev-main#33004e6 as 2.1", "shlinkio/shlink-event-dispatcher": "dev-main#48c0137 as 2.6", "shlinkio/shlink-importer": "^4.0", - "shlinkio/shlink-installer": "^8.1", + "shlinkio/shlink-installer": "dev-develop#f1cc5c7 as 8.2", "shlinkio/shlink-ip-geolocation": "^3.0", "spiral/roadrunner": "^2.11", "spiral/roadrunner-jobs": "^2.3", diff --git a/config/autoload/installer.global.php b/config/autoload/installer.global.php index 2e120e35..a7a4c97a 100644 --- a/config/autoload/installer.global.php +++ b/config/autoload/installer.global.php @@ -44,6 +44,7 @@ return [ Option\UrlShortener\AutoResolveTitlesConfigOption::class, Option\UrlShortener\AppendExtraPathConfigOption::class, Option\UrlShortener\EnableMultiSegmentSlugsConfigOption::class, + Option\UrlShortener\EnableTrailingSlashConfigOption::class, Option\Tracking\IpAnonymizationConfigOption::class, Option\Tracking\OrphanVisitsTrackingConfigOption::class, Option\Tracking\DisableTrackParamConfigOption::class, From d97cabbe795f5e3941fe0ca6dda6893a77229e1c Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Mon, 5 Sep 2022 21:14:53 +0200 Subject: [PATCH 073/108] Updated changelog --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f4ef358..12c68daa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this ## [Unreleased] ### Added +* [#1221](https://github.com/shlinkio/shlink/issues/1221) Added experimental support to run Shlink with [RoadRunner](https://roadrunner.dev) instead of openswoole. +* [#1531](https://github.com/shlinkio/shlink/issues/1531) and [#1090](https://github.com/shlinkio/shlink/issues/1090) Added support for trailing slashes in short URLs. * [#1406](https://github.com/shlinkio/shlink/issues/1406) Added new REST API version 3. When making requests to the REST API with `/rest/v3/...` and an error occurs, all error types will be different, with the next correlation: @@ -28,7 +30,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this Non-error responses are not affected. * [#1513](https://github.com/shlinkio/shlink/issues/1513) Added publishing of the docker image in GHCR. -* [#1221](https://github.com/shlinkio/shlink/issues/1221) Added experimental support to run Shlink with [RoadRunner](https://roadrunner.dev) instead of openswoole. ### Changed * [#1339](https://github.com/shlinkio/shlink/issues/1339) Added new test suite for CLI E2E tests. From e712efd0086b7000bd7af06290740aba30a23d70 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Tue, 6 Sep 2022 21:56:18 +0200 Subject: [PATCH 074/108] Simplified exception --- .../src/Exception/GeolocationDbUpdateFailedException.php | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/module/CLI/src/Exception/GeolocationDbUpdateFailedException.php b/module/CLI/src/Exception/GeolocationDbUpdateFailedException.php index ef59d225..cbb8affd 100644 --- a/module/CLI/src/Exception/GeolocationDbUpdateFailedException.php +++ b/module/CLI/src/Exception/GeolocationDbUpdateFailedException.php @@ -13,16 +13,15 @@ class GeolocationDbUpdateFailedException extends RuntimeException implements Exc { private bool $olderDbExists; - private function __construct(string $message, int $code, ?Throwable $previous) + private function __construct(string $message, ?Throwable $previous = null) { - parent::__construct($message, $code, $previous); + parent::__construct($message, 0, $previous); } public static function withOlderDb(?Throwable $prev = null): self { $e = new self( 'An error occurred while updating geolocation database, but an older DB is already present.', - 0, $prev, ); $e->olderDbExists = true; @@ -34,7 +33,6 @@ class GeolocationDbUpdateFailedException extends RuntimeException implements Exc { $e = new self( 'An error occurred while updating geolocation database, and an older version could not be found.', - 0, $prev, ); $e->olderDbExists = false; @@ -47,7 +45,7 @@ class GeolocationDbUpdateFailedException extends RuntimeException implements Exc $e = new self(sprintf( 'Build epoch with value "%s" from existing geolocation database, could not be parsed to integer.', $buildEpoch, - ), 0, null); + )); $e->olderDbExists = true; return $e; From b078c00492b8d53ce6468ce9485941ce7156f1c5 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Thu, 8 Sep 2022 14:10:09 +0200 Subject: [PATCH 075/108] Migrated to custom doctrine cli entry point, as the built-in is deprecated --- bin/doctrine | 12 ++++++++++++ composer.json | 2 +- config/autoload/installer.global.php | 6 ++++++ config/test/bootstrap_api_tests.php | 7 ++++++- config/test/bootstrap_cli_tests.php | 7 ++++++- config/test/bootstrap_db_tests.php | 7 ++++++- docker/docker-entrypoint.sh | 4 ++-- module/CLI/src/Command/Db/CreateDatabaseCommand.php | 2 +- 8 files changed, 40 insertions(+), 7 deletions(-) create mode 100755 bin/doctrine diff --git a/bin/doctrine b/bin/doctrine new file mode 100755 index 00000000..4fec1714 --- /dev/null +++ b/bin/doctrine @@ -0,0 +1,12 @@ +#!/usr/bin/env php +value => [ 'command' => 'bin/cli ' . Command\Db\MigrateDatabaseCommand::NAME, ], + InstallationCommand::ORM_PROXIES->value => [ + 'command' => 'bin/doctrine orm:generate-proxies', + ], + InstallationCommand::ORM_CLEAR_CACHE->value => [ + 'command' => 'bin/doctrine orm:clear-cache:metadata', + ], InstallationCommand::GEOLITE_DOWNLOAD_DB->value => [ 'command' => 'bin/cli ' . Command\Visit\DownloadGeoLiteDbCommand::NAME, ], diff --git a/config/test/bootstrap_api_tests.php b/config/test/bootstrap_api_tests.php index bc119284..2653b552 100644 --- a/config/test/bootstrap_api_tests.php +++ b/config/test/bootstrap_api_tests.php @@ -28,6 +28,11 @@ register_shutdown_function(function () use ($httpClient): void { ); }); -$testHelper->createTestDb(['bin/cli', 'db:create'], ['bin/cli', 'db:migrate']); +$testHelper->createTestDb( + ['bin/cli', 'db:create'], + ['bin/cli', 'db:migrate'], + ['bin/doctrine', 'orm:schema-tool:drop'], + ['bin/doctrine', 'dbal:run-sql'], +); ApiTest\ApiTestCase::setApiClient($httpClient); ApiTest\ApiTestCase::setSeedFixturesCallback(fn () => $testHelper->seedFixtures($em, $config['data_fixtures'] ?? [])); diff --git a/config/test/bootstrap_cli_tests.php b/config/test/bootstrap_cli_tests.php index 893bdfd7..c8c33721 100644 --- a/config/test/bootstrap_cli_tests.php +++ b/config/test/bootstrap_cli_tests.php @@ -22,7 +22,12 @@ if (file_exists($covFile)) { unlink($covFile); } -$testHelper->createTestDb(['bin/cli', 'db:create'], ['bin/cli', 'db:migrate']); +$testHelper->createTestDb( + ['bin/cli', 'db:create'], + ['bin/cli', 'db:migrate'], + ['bin/doctrine', 'orm:schema-tool:drop'], + ['bin/doctrine', 'dbal:run-sql'], +); CliTest\CliTestCase::setSeedFixturesCallback( static fn () => $testHelper->seedFixtures($em, $config['data_fixtures'] ?? []), ); diff --git a/config/test/bootstrap_db_tests.php b/config/test/bootstrap_db_tests.php index 0237d741..5aa8ea51 100644 --- a/config/test/bootstrap_db_tests.php +++ b/config/test/bootstrap_db_tests.php @@ -8,5 +8,10 @@ use Psr\Container\ContainerInterface; /** @var ContainerInterface $container */ $container = require __DIR__ . '/../container.php'; -$container->get(Helper\TestHelper::class)->createTestDb(['bin/cli', 'db:create'], ['bin/cli', 'db:migrate']); +$container->get(Helper\TestHelper::class)->createTestDb( + ['bin/cli', 'db:create'], + ['bin/cli', 'db:migrate'], + ['bin/doctrine', 'orm:schema-tool:drop'], + ['bin/doctrine', 'dbal:run-sql'], +); DbTest\DatabaseTestCase::setEntityManager($container->get('em')); diff --git a/docker/docker-entrypoint.sh b/docker/docker-entrypoint.sh index 1955f0ec..f28627d2 100644 --- a/docker/docker-entrypoint.sh +++ b/docker/docker-entrypoint.sh @@ -13,10 +13,10 @@ echo "Updating database..." php bin/cli db:migrate -n ${flags} echo "Generating proxies..." -php vendor/doctrine/orm/bin/doctrine.php orm:generate-proxies -n ${flags} +php bin/doctrine orm:generate-proxies -n ${flags} echo "Clearing entities cache..." -php vendor/doctrine/orm/bin/doctrine.php orm:clear-cache:metadata -n ${flags} +php bin/doctrine orm:clear-cache:metadata -n ${flags} # Try to download GeoLite2 db file only if the license key env var was defined if [ ! -z "${GEOLITE_LICENSE_KEY}" ]; then diff --git a/module/CLI/src/Command/Db/CreateDatabaseCommand.php b/module/CLI/src/Command/Db/CreateDatabaseCommand.php index 415290a3..5cc6a184 100644 --- a/module/CLI/src/Command/Db/CreateDatabaseCommand.php +++ b/module/CLI/src/Command/Db/CreateDatabaseCommand.php @@ -22,7 +22,7 @@ use const Shlinkio\Shlink\MIGRATIONS_TABLE; class CreateDatabaseCommand extends AbstractDatabaseCommand { public const NAME = 'db:create'; - public const DOCTRINE_SCRIPT = 'vendor/doctrine/orm/bin/doctrine.php'; + public const DOCTRINE_SCRIPT = 'bin/doctrine'; public const DOCTRINE_CREATE_SCHEMA_COMMAND = 'orm:schema-tool:create'; public function __construct( From 5b78b363f0c81b54df5e06b674c7ebea2a911d5d Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Thu, 8 Sep 2022 14:11:01 +0200 Subject: [PATCH 076/108] Updated changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 12c68daa..921e45d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this ### Changed * [#1339](https://github.com/shlinkio/shlink/issues/1339) Added new test suite for CLI E2E tests. * [#1503](https://github.com/shlinkio/shlink/issues/1503) Drastically improved build time in GitHub Actions, by optimizing parallelization, adding php extensions cache and running mutation tests only for changed files. +* [#1525](https://github.com/shlinkio/shlink/issues/1525) Migrated to custom doctrine CLI entry point. ### Deprecated * *Nothing* From e6ee4ceae20bc5cfde92b388e862f15843b90718 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Thu, 8 Sep 2022 20:50:11 +0200 Subject: [PATCH 077/108] Simplified mapping of TagInfo objects --- module/Core/src/Repository/TagRepository.php | 2 +- module/Core/src/Tag/Model/TagInfo.php | 5 +++++ module/Core/src/Tag/Model/TagsParams.php | 1 + 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/module/Core/src/Repository/TagRepository.php b/module/Core/src/Repository/TagRepository.php index 2c4e8db6..946cee7e 100644 --- a/module/Core/src/Repository/TagRepository.php +++ b/module/Core/src/Repository/TagRepository.php @@ -110,7 +110,7 @@ class TagRepository extends EntitySpecificationRepository implements TagReposito return map( $this->getEntityManager()->createNativeQuery($nativeQb->getSQL(), $rsm)->getResult(), - static fn (array $row) => new TagInfo($row['tag'], (int) $row['shortUrlsCount'], (int) $row['visitsCount']), + TagInfo::fromRawData(...), ); } diff --git a/module/Core/src/Tag/Model/TagInfo.php b/module/Core/src/Tag/Model/TagInfo.php index 8a4f196b..5e71ea5b 100644 --- a/module/Core/src/Tag/Model/TagInfo.php +++ b/module/Core/src/Tag/Model/TagInfo.php @@ -15,6 +15,11 @@ final class TagInfo implements JsonSerializable ) { } + public static function fromRawData(array $data): self + { + return new self($data['tag'], (int) $data['shortUrlsCount'], (int) $data['visitsCount']); + } + public function jsonSerialize(): array { return [ diff --git a/module/Core/src/Tag/Model/TagsParams.php b/module/Core/src/Tag/Model/TagsParams.php index 633fd5f2..3b1d84b2 100644 --- a/module/Core/src/Tag/Model/TagsParams.php +++ b/module/Core/src/Tag/Model/TagsParams.php @@ -14,6 +14,7 @@ final class TagsParams extends AbstractInfinitePaginableListParams private function __construct( public readonly ?string $searchTerm, public readonly Ordering $orderBy, + /** @deprecated */ public readonly bool $withStats, ?int $page, ?int $itemsPerPage, From 63ceba199d4f856a000bee6d1cb650aeb20f1ee7 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 11 Sep 2022 09:03:25 +0200 Subject: [PATCH 078/108] Removed mention to improvement on mutation tests in changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 921e45d4..99614a6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,7 +33,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this ### Changed * [#1339](https://github.com/shlinkio/shlink/issues/1339) Added new test suite for CLI E2E tests. -* [#1503](https://github.com/shlinkio/shlink/issues/1503) Drastically improved build time in GitHub Actions, by optimizing parallelization, adding php extensions cache and running mutation tests only for changed files. +* [#1503](https://github.com/shlinkio/shlink/issues/1503) Drastically improved build time in GitHub Actions, by optimizing parallelization and adding php extensions cache. * [#1525](https://github.com/shlinkio/shlink/issues/1525) Migrated to custom doctrine CLI entry point. ### Deprecated From f5138385befb1b673edcfd470a8cd61abe937f23 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 11 Sep 2022 10:45:03 +0200 Subject: [PATCH 079/108] Created new env var to programatically provide an initial API key --- config/autoload/common.global.php | 4 +-- module/Core/src/Config/EnvVars.php | 1 + .../Shlinkio.Shlink.Rest.Entity.ApiKey.php | 3 +- module/Rest/config/initial-api-key.config.php | 24 ++++++++++++++ .../src/ApiKey/InitialApiKeyDelegator.php | 31 +++++++++++++++++++ .../ApiKey/Repository/ApiKeyRepository.php | 29 +++++++++++++++++ .../Repository/ApiKeyRepositoryInterface.php | 16 ++++++++++ module/Rest/src/Entity/ApiKey.php | 18 +++++++---- module/Rest/test/ConfigProviderTest.php | 3 +- 9 files changed, 119 insertions(+), 10 deletions(-) create mode 100644 module/Rest/config/initial-api-key.config.php create mode 100644 module/Rest/src/ApiKey/InitialApiKeyDelegator.php create mode 100644 module/Rest/src/ApiKey/Repository/ApiKeyRepository.php create mode 100644 module/Rest/src/ApiKey/Repository/ApiKeyRepositoryInterface.php diff --git a/config/autoload/common.global.php b/config/autoload/common.global.php index b35807fd..19404d8c 100644 --- a/config/autoload/common.global.php +++ b/config/autoload/common.global.php @@ -8,8 +8,8 @@ return [ 'debug' => false, - // Disabling config cache for cli, ensures it's never used for openswoole and also that console commands don't - // generate a cache file that's then used by non-openswoole web executions + // Disabling config cache for cli, ensures it's never used for openswoole/RoadRunner, and also that console + // commands don't generate a cache file that's then used by php-fpm web executions ConfigAggregator::ENABLE_CACHE => PHP_SAPI !== 'cli', ]; diff --git a/module/Core/src/Config/EnvVars.php b/module/Core/src/Config/EnvVars.php index 7cbd0af4..228a5921 100644 --- a/module/Core/src/Config/EnvVars.php +++ b/module/Core/src/Config/EnvVars.php @@ -47,6 +47,7 @@ enum EnvVars: string case PORT = 'PORT'; case TASK_WORKER_NUM = 'TASK_WORKER_NUM'; case WEB_WORKER_NUM = 'WEB_WORKER_NUM'; + case INITIAL_API_KEY = 'INITIAL_API_KEY'; case ANONYMIZE_REMOTE_ADDR = 'ANONYMIZE_REMOTE_ADDR'; case TRACK_ORPHAN_VISITS = 'TRACK_ORPHAN_VISITS'; case DISABLE_TRACK_PARAM = 'DISABLE_TRACK_PARAM'; diff --git a/module/Rest/config/entities-mappings/Shlinkio.Shlink.Rest.Entity.ApiKey.php b/module/Rest/config/entities-mappings/Shlinkio.Shlink.Rest.Entity.ApiKey.php index 63716e74..1e0b041b 100644 --- a/module/Rest/config/entities-mappings/Shlinkio.Shlink.Rest.Entity.ApiKey.php +++ b/module/Rest/config/entities-mappings/Shlinkio.Shlink.Rest.Entity.ApiKey.php @@ -15,7 +15,8 @@ use function Shlinkio\Shlink\Core\determineTableName; return static function (ClassMetadata $metadata, array $emConfig): void { $builder = new ClassMetadataBuilder($metadata); - $builder->setTable(determineTableName('api_keys', $emConfig)); + $builder->setTable(determineTableName('api_keys', $emConfig)) + ->setCustomRepositoryClass(ApiKey\Repository\ApiKeyRepository::class); $builder->createField('id', Types::BIGINT) ->makePrimaryKey() diff --git a/module/Rest/config/initial-api-key.config.php b/module/Rest/config/initial-api-key.config.php new file mode 100644 index 00000000..57114479 --- /dev/null +++ b/module/Rest/config/initial-api-key.config.php @@ -0,0 +1,24 @@ + PHP_SAPI !== 'cli' ? null : EnvVars::INITIAL_API_KEY->loadFromEnv(), + + 'dependencies' => [ + 'delegators' => [ + Application::class => [ + ApiKey\InitialApiKeyDelegator::class, + ], + ], + ], + +]; diff --git a/module/Rest/src/ApiKey/InitialApiKeyDelegator.php b/module/Rest/src/ApiKey/InitialApiKeyDelegator.php new file mode 100644 index 00000000..9129d7d3 --- /dev/null +++ b/module/Rest/src/ApiKey/InitialApiKeyDelegator.php @@ -0,0 +1,31 @@ +get('config')['initial_api_key'] ?? null; + if ($initialApiKey !== null) { + $this->createInitialApiKey($initialApiKey, $container); + } + + return $callback(); + } + + private function createInitialApiKey(string $initialApiKey, ContainerInterface $container): void + { + /** @var ApiKeyRepositoryInterface $repo */ + $repo = $container->get(EntityManager::class)->getRepository(ApiKey::class); + $repo->createInitialApiKey($initialApiKey); + } +} diff --git a/module/Rest/src/ApiKey/Repository/ApiKeyRepository.php b/module/Rest/src/ApiKey/Repository/ApiKeyRepository.php new file mode 100644 index 00000000..098dda6a --- /dev/null +++ b/module/Rest/src/ApiKey/Repository/ApiKeyRepository.php @@ -0,0 +1,29 @@ +getEntityManager()->wrapInTransaction(function () use ($apiKey): void { + $qb = $this->getEntityManager()->createQueryBuilder(); + $amountOfApiKeys = $qb->select('COUNT(a.id)') + ->from(ApiKey::class, 'a') + ->getQuery() + ->setLockMode(LockMode::PESSIMISTIC_WRITE) + ->getSingleScalarResult(); + + if ($amountOfApiKeys === 0) { + $this->getEntityManager()->persist(ApiKey::fromKey($apiKey)); + $this->getEntityManager()->flush(); + } + }); + } +} diff --git a/module/Rest/src/ApiKey/Repository/ApiKeyRepositoryInterface.php b/module/Rest/src/ApiKey/Repository/ApiKeyRepositoryInterface.php new file mode 100644 index 00000000..f5beb3e9 --- /dev/null +++ b/module/Rest/src/ApiKey/Repository/ApiKeyRepositoryInterface.php @@ -0,0 +1,16 @@ +key = Uuid::uuid4()->toString(); - $this->expirationDate = $expirationDate; - $this->name = $name; + $this->key = $key ?? Uuid::uuid4()->toString(); $this->enabled = true; $this->roles = new ArrayCollection(); } @@ -44,7 +42,10 @@ class ApiKey extends AbstractEntity public static function fromMeta(ApiKeyMeta $meta): self { - $apiKey = new self($meta->name, $meta->expirationDate); + $apiKey = self::create(); + $apiKey->name = $meta->name; + $apiKey->expirationDate = $meta->expirationDate; + foreach ($meta->roleDefinitions as $roleDefinition) { $apiKey->registerRole($roleDefinition); } @@ -52,6 +53,11 @@ class ApiKey extends AbstractEntity return $apiKey; } + public static function fromKey(string $key): self + { + return new self($key); + } + public function getExpirationDate(): ?Chronos { return $this->expirationDate; diff --git a/module/Rest/test/ConfigProviderTest.php b/module/Rest/test/ConfigProviderTest.php index d3288151..06bf179f 100644 --- a/module/Rest/test/ConfigProviderTest.php +++ b/module/Rest/test/ConfigProviderTest.php @@ -22,10 +22,11 @@ class ConfigProviderTest extends TestCase { $config = ($this->configProvider)(); - self::assertCount(4, $config); + self::assertCount(5, $config); self::assertArrayHasKey('dependencies', $config); self::assertArrayHasKey('auth', $config); self::assertArrayHasKey('entity_manager', $config); + self::assertArrayHasKey('initial_api_key', $config); self::assertArrayHasKey(ConfigAbstractFactory::class, $config); } From c841e57db5077b49329dc7b125ac02fcb4a3f8d3 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 11 Sep 2022 11:59:49 +0200 Subject: [PATCH 080/108] Reduced duplication in ApiKeyRepository --- .../src/ApiKey/Repository/ApiKeyRepository.php | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/module/Rest/src/ApiKey/Repository/ApiKeyRepository.php b/module/Rest/src/ApiKey/Repository/ApiKeyRepository.php index 098dda6a..46c1e1a5 100644 --- a/module/Rest/src/ApiKey/Repository/ApiKeyRepository.php +++ b/module/Rest/src/ApiKey/Repository/ApiKeyRepository.php @@ -12,17 +12,17 @@ class ApiKeyRepository extends EntitySpecificationRepository implements ApiKeyRe { public function createInitialApiKey(string $apiKey): void { - $this->getEntityManager()->wrapInTransaction(function () use ($apiKey): void { - $qb = $this->getEntityManager()->createQueryBuilder(); - $amountOfApiKeys = $qb->select('COUNT(a.id)') - ->from(ApiKey::class, 'a') - ->getQuery() - ->setLockMode(LockMode::PESSIMISTIC_WRITE) - ->getSingleScalarResult(); + $em = $this->getEntityManager(); + $em->wrapInTransaction(function () use ($apiKey, $em): void { + $amountOfApiKeys = $em->createQueryBuilder()->select('COUNT(a.id)') + ->from(ApiKey::class, 'a') + ->getQuery() + ->setLockMode(LockMode::PESSIMISTIC_WRITE) + ->getSingleScalarResult(); if ($amountOfApiKeys === 0) { - $this->getEntityManager()->persist(ApiKey::fromKey($apiKey)); - $this->getEntityManager()->flush(); + $em->persist(ApiKey::fromKey($apiKey)); + $em->flush(); } }); } From 997289da027829d4aa4d2303111ee5d58090c8f6 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 11 Sep 2022 12:02:49 +0200 Subject: [PATCH 081/108] Changed all public setUp methods in tests to be protected --- module/CLI/test/Command/Api/DisableKeyCommandTest.php | 2 +- module/CLI/test/Command/Api/GenerateKeyCommandTest.php | 2 +- module/CLI/test/Command/Api/ListKeysCommandTest.php | 2 +- module/CLI/test/Command/Db/CreateDatabaseCommandTest.php | 2 +- module/CLI/test/Command/Db/MigrateDatabaseCommandTest.php | 2 +- module/CLI/test/Command/Domain/DomainRedirectsCommandTest.php | 2 +- module/CLI/test/Command/Domain/ListDomainsCommandTest.php | 2 +- module/CLI/test/Command/ShortUrl/CreateShortUrlCommandTest.php | 2 +- module/CLI/test/Command/ShortUrl/DeleteShortUrlCommandTest.php | 2 +- .../CLI/test/Command/ShortUrl/GetShortUrlVisitsCommandTest.php | 2 +- module/CLI/test/Command/ShortUrl/ListShortUrlsCommandTest.php | 2 +- module/CLI/test/Command/ShortUrl/ResolveUrlCommandTest.php | 2 +- module/CLI/test/Command/Tag/DeleteTagsCommandTest.php | 2 +- module/CLI/test/Command/Tag/ListTagsCommandTest.php | 2 +- module/CLI/test/Command/Tag/RenameTagCommandTest.php | 2 +- module/CLI/test/Command/Visit/LocateVisitsCommandTest.php | 2 +- module/CLI/test/ConfigProviderTest.php | 2 +- module/CLI/test/Factory/ApplicationFactoryTest.php | 2 +- module/CLI/test/Util/GeolocationDbUpdaterTest.php | 2 +- module/CLI/test/Util/ShlinkTableTest.php | 2 +- module/Core/test/Action/PixelActionTest.php | 2 +- module/Core/test/Action/QrCodeActionTest.php | 2 +- module/Core/test/Action/RedirectActionTest.php | 2 +- module/Core/test/Config/BasePathPrefixerTest.php | 2 +- module/Core/test/ConfigProviderTest.php | 2 +- module/Core/test/Domain/DomainServiceTest.php | 2 +- module/Core/test/ErrorHandler/NotFoundRedirectHandlerTest.php | 2 +- module/Core/test/ErrorHandler/NotFoundTemplateHandlerTest.php | 2 +- .../CloseDbConnectionEventListenerDelegatorTest.php | 2 +- .../test/EventDispatcher/CloseDbConnectionEventListenerTest.php | 2 +- module/Core/test/EventDispatcher/LocateVisitTest.php | 2 +- .../test/EventDispatcher/Mercure/NotifyVisitToMercureTest.php | 2 +- module/Core/test/EventDispatcher/NotifyVisitToWebHooksTest.php | 2 +- .../test/EventDispatcher/PublishingUpdatesGeneratorTest.php | 2 +- module/Core/test/Service/ShortUrl/DeleteShortUrlServiceTest.php | 2 +- module/Core/test/Service/ShortUrl/ShortUrlResolverTest.php | 2 +- module/Core/test/Service/ShortUrlServiceTest.php | 2 +- module/Core/test/Service/UrlShortenerTest.php | 2 +- .../Paginator/Adapter/ShortUrlRepositoryAdapterTest.php | 2 +- .../Resolver/PersistenceShortUrlRelationResolverTest.php | 2 +- .../ShortUrl/Resolver/SimpleShortUrlRelationResolverTest.php | 2 +- .../test/ShortUrl/Transformer/ShortUrlDataTransformerTest.php | 2 +- module/Core/test/Tag/TagServiceTest.php | 2 +- module/Core/test/Util/UrlValidatorTest.php | 2 +- module/Core/test/Visit/VisitLocatorTest.php | 2 +- module/Core/test/Visit/VisitsStatsHelperTest.php | 2 +- module/Core/test/Visit/VisitsTrackerTest.php | 2 +- module/Rest/test/Action/Domain/ListDomainsActionTest.php | 2 +- module/Rest/test/Action/HealthActionTest.php | 2 +- module/Rest/test/Action/MercureInfoActionTest.php | 2 +- module/Rest/test/Action/ShortUrl/CreateShortUrlActionTest.php | 2 +- module/Rest/test/Action/ShortUrl/DeleteShortUrlActionTest.php | 2 +- module/Rest/test/Action/ShortUrl/EditShortUrlActionTest.php | 2 +- module/Rest/test/Action/ShortUrl/ListShortUrlsActionTest.php | 2 +- module/Rest/test/Action/ShortUrl/ResolveShortUrlActionTest.php | 2 +- .../test/Action/ShortUrl/SingleStepCreateShortUrlActionTest.php | 2 +- module/Rest/test/Action/Tag/DeleteTagsActionTest.php | 2 +- module/Rest/test/Action/Tag/ListTagsActionTest.php | 2 +- module/Rest/test/Action/Tag/TagsStatsActionTest.php | 2 +- module/Rest/test/Action/Tag/UpdateTagActionTest.php | 2 +- module/Rest/test/Action/Visit/GlobalVisitsActionTest.php | 2 +- module/Rest/test/Action/Visit/NonOrphanVisitsActionTest.php | 2 +- module/Rest/test/Action/Visit/ShortUrlVisitsActionTest.php | 2 +- module/Rest/test/ConfigProviderTest.php | 2 +- module/Rest/test/Middleware/AuthenticationMiddlewareTest.php | 2 +- module/Rest/test/Middleware/BodyParserMiddlewareTest.php | 2 +- module/Rest/test/Middleware/CrossDomainMiddlewareTest.php | 2 +- .../EmptyResponseImplicitOptionsMiddlewareFactoryTest.php | 2 +- .../ShortUrl/CreateShortUrlContentNegotiationMiddlewareTest.php | 2 +- .../ShortUrl/DefaultShortCodesLengthMiddlewareTest.php | 2 +- .../ShortUrl/DropDefaultDomainFromRequestMiddlewareTest.php | 2 +- module/Rest/test/Service/ApiKeyServiceTest.php | 2 +- 72 files changed, 72 insertions(+), 72 deletions(-) diff --git a/module/CLI/test/Command/Api/DisableKeyCommandTest.php b/module/CLI/test/Command/Api/DisableKeyCommandTest.php index 90942dc9..41a4f982 100644 --- a/module/CLI/test/Command/Api/DisableKeyCommandTest.php +++ b/module/CLI/test/Command/Api/DisableKeyCommandTest.php @@ -19,7 +19,7 @@ class DisableKeyCommandTest extends TestCase private CommandTester $commandTester; private ObjectProphecy $apiKeyService; - public function setUp(): void + protected function setUp(): void { $this->apiKeyService = $this->prophesize(ApiKeyServiceInterface::class); $this->commandTester = $this->testerForCommand(new DisableKeyCommand($this->apiKeyService->reveal())); diff --git a/module/CLI/test/Command/Api/GenerateKeyCommandTest.php b/module/CLI/test/Command/Api/GenerateKeyCommandTest.php index e5c543d5..6db8581b 100644 --- a/module/CLI/test/Command/Api/GenerateKeyCommandTest.php +++ b/module/CLI/test/Command/Api/GenerateKeyCommandTest.php @@ -23,7 +23,7 @@ class GenerateKeyCommandTest extends TestCase private CommandTester $commandTester; private ObjectProphecy $apiKeyService; - public function setUp(): void + protected function setUp(): void { $this->apiKeyService = $this->prophesize(ApiKeyServiceInterface::class); $roleResolver = $this->prophesize(RoleResolverInterface::class); diff --git a/module/CLI/test/Command/Api/ListKeysCommandTest.php b/module/CLI/test/Command/Api/ListKeysCommandTest.php index 68c1e844..7122f392 100644 --- a/module/CLI/test/Command/Api/ListKeysCommandTest.php +++ b/module/CLI/test/Command/Api/ListKeysCommandTest.php @@ -23,7 +23,7 @@ class ListKeysCommandTest extends TestCase private CommandTester $commandTester; private ObjectProphecy $apiKeyService; - public function setUp(): void + protected function setUp(): void { $this->apiKeyService = $this->prophesize(ApiKeyServiceInterface::class); $this->commandTester = $this->testerForCommand(new ListKeysCommand($this->apiKeyService->reveal())); diff --git a/module/CLI/test/Command/Db/CreateDatabaseCommandTest.php b/module/CLI/test/Command/Db/CreateDatabaseCommandTest.php index 93e07d4d..f500775a 100644 --- a/module/CLI/test/Command/Db/CreateDatabaseCommandTest.php +++ b/module/CLI/test/Command/Db/CreateDatabaseCommandTest.php @@ -33,7 +33,7 @@ class CreateDatabaseCommandTest extends TestCase private ObjectProphecy $schemaManager; private ObjectProphecy $driver; - public function setUp(): void + protected function setUp(): void { $locker = $this->prophesize(LockFactory::class); $lock = $this->prophesize(LockInterface::class); diff --git a/module/CLI/test/Command/Db/MigrateDatabaseCommandTest.php b/module/CLI/test/Command/Db/MigrateDatabaseCommandTest.php index d301f55e..1a8dfb0e 100644 --- a/module/CLI/test/Command/Db/MigrateDatabaseCommandTest.php +++ b/module/CLI/test/Command/Db/MigrateDatabaseCommandTest.php @@ -23,7 +23,7 @@ class MigrateDatabaseCommandTest extends TestCase private CommandTester $commandTester; private ObjectProphecy $processHelper; - public function setUp(): void + protected function setUp(): void { $locker = $this->prophesize(LockFactory::class); $lock = $this->prophesize(LockInterface::class); diff --git a/module/CLI/test/Command/Domain/DomainRedirectsCommandTest.php b/module/CLI/test/Command/Domain/DomainRedirectsCommandTest.php index 6b6e1036..afcce551 100644 --- a/module/CLI/test/Command/Domain/DomainRedirectsCommandTest.php +++ b/module/CLI/test/Command/Domain/DomainRedirectsCommandTest.php @@ -24,7 +24,7 @@ class DomainRedirectsCommandTest extends TestCase private CommandTester $commandTester; private ObjectProphecy $domainService; - public function setUp(): void + protected function setUp(): void { $this->domainService = $this->prophesize(DomainServiceInterface::class); $this->commandTester = $this->testerForCommand(new DomainRedirectsCommand($this->domainService->reveal())); diff --git a/module/CLI/test/Command/Domain/ListDomainsCommandTest.php b/module/CLI/test/Command/Domain/ListDomainsCommandTest.php index 6d56ea69..adaa1d00 100644 --- a/module/CLI/test/Command/Domain/ListDomainsCommandTest.php +++ b/module/CLI/test/Command/Domain/ListDomainsCommandTest.php @@ -23,7 +23,7 @@ class ListDomainsCommandTest extends TestCase private CommandTester $commandTester; private ObjectProphecy $domainService; - public function setUp(): void + protected function setUp(): void { $this->domainService = $this->prophesize(DomainServiceInterface::class); $this->commandTester = $this->testerForCommand(new ListDomainsCommand($this->domainService->reveal())); diff --git a/module/CLI/test/Command/ShortUrl/CreateShortUrlCommandTest.php b/module/CLI/test/Command/ShortUrl/CreateShortUrlCommandTest.php index 73d2b785..e529f0ad 100644 --- a/module/CLI/test/Command/ShortUrl/CreateShortUrlCommandTest.php +++ b/module/CLI/test/Command/ShortUrl/CreateShortUrlCommandTest.php @@ -30,7 +30,7 @@ class CreateShortUrlCommandTest extends TestCase private ObjectProphecy $urlShortener; private ObjectProphecy $stringifier; - public function setUp(): void + protected function setUp(): void { $this->urlShortener = $this->prophesize(UrlShortener::class); $this->stringifier = $this->prophesize(ShortUrlStringifierInterface::class); diff --git a/module/CLI/test/Command/ShortUrl/DeleteShortUrlCommandTest.php b/module/CLI/test/Command/ShortUrl/DeleteShortUrlCommandTest.php index 947b7443..80d4878c 100644 --- a/module/CLI/test/Command/ShortUrl/DeleteShortUrlCommandTest.php +++ b/module/CLI/test/Command/ShortUrl/DeleteShortUrlCommandTest.php @@ -26,7 +26,7 @@ class DeleteShortUrlCommandTest extends TestCase private CommandTester $commandTester; private ObjectProphecy $service; - public function setUp(): void + protected function setUp(): void { $this->service = $this->prophesize(DeleteShortUrlServiceInterface::class); $this->commandTester = $this->testerForCommand(new DeleteShortUrlCommand($this->service->reveal())); diff --git a/module/CLI/test/Command/ShortUrl/GetShortUrlVisitsCommandTest.php b/module/CLI/test/Command/ShortUrl/GetShortUrlVisitsCommandTest.php index 316c762e..644a0b8f 100644 --- a/module/CLI/test/Command/ShortUrl/GetShortUrlVisitsCommandTest.php +++ b/module/CLI/test/Command/ShortUrl/GetShortUrlVisitsCommandTest.php @@ -33,7 +33,7 @@ class GetShortUrlVisitsCommandTest extends TestCase private CommandTester $commandTester; private ObjectProphecy $visitsHelper; - public function setUp(): void + protected function setUp(): void { $this->visitsHelper = $this->prophesize(VisitsStatsHelperInterface::class); $command = new GetShortUrlVisitsCommand($this->visitsHelper->reveal()); diff --git a/module/CLI/test/Command/ShortUrl/ListShortUrlsCommandTest.php b/module/CLI/test/Command/ShortUrl/ListShortUrlsCommandTest.php index f9d701cb..51b02799 100644 --- a/module/CLI/test/Command/ShortUrl/ListShortUrlsCommandTest.php +++ b/module/CLI/test/Command/ShortUrl/ListShortUrlsCommandTest.php @@ -33,7 +33,7 @@ class ListShortUrlsCommandTest extends TestCase private CommandTester $commandTester; private ObjectProphecy $shortUrlService; - public function setUp(): void + protected function setUp(): void { $this->shortUrlService = $this->prophesize(ShortUrlServiceInterface::class); $command = new ListShortUrlsCommand($this->shortUrlService->reveal(), new ShortUrlDataTransformer( diff --git a/module/CLI/test/Command/ShortUrl/ResolveUrlCommandTest.php b/module/CLI/test/Command/ShortUrl/ResolveUrlCommandTest.php index 12e29eaf..24974692 100644 --- a/module/CLI/test/Command/ShortUrl/ResolveUrlCommandTest.php +++ b/module/CLI/test/Command/ShortUrl/ResolveUrlCommandTest.php @@ -25,7 +25,7 @@ class ResolveUrlCommandTest extends TestCase private CommandTester $commandTester; private ObjectProphecy $urlResolver; - public function setUp(): void + protected function setUp(): void { $this->urlResolver = $this->prophesize(ShortUrlResolverInterface::class); $this->commandTester = $this->testerForCommand(new ResolveUrlCommand($this->urlResolver->reveal())); diff --git a/module/CLI/test/Command/Tag/DeleteTagsCommandTest.php b/module/CLI/test/Command/Tag/DeleteTagsCommandTest.php index 46f61814..b03bf1ee 100644 --- a/module/CLI/test/Command/Tag/DeleteTagsCommandTest.php +++ b/module/CLI/test/Command/Tag/DeleteTagsCommandTest.php @@ -18,7 +18,7 @@ class DeleteTagsCommandTest extends TestCase private CommandTester $commandTester; private ObjectProphecy $tagService; - public function setUp(): void + protected function setUp(): void { $this->tagService = $this->prophesize(TagServiceInterface::class); $this->commandTester = $this->testerForCommand(new DeleteTagsCommand($this->tagService->reveal())); diff --git a/module/CLI/test/Command/Tag/ListTagsCommandTest.php b/module/CLI/test/Command/Tag/ListTagsCommandTest.php index 499442d0..58ae1ef1 100644 --- a/module/CLI/test/Command/Tag/ListTagsCommandTest.php +++ b/module/CLI/test/Command/Tag/ListTagsCommandTest.php @@ -22,7 +22,7 @@ class ListTagsCommandTest extends TestCase private CommandTester $commandTester; private ObjectProphecy $tagService; - public function setUp(): void + protected function setUp(): void { $this->tagService = $this->prophesize(TagServiceInterface::class); $this->commandTester = $this->testerForCommand(new ListTagsCommand($this->tagService->reveal())); diff --git a/module/CLI/test/Command/Tag/RenameTagCommandTest.php b/module/CLI/test/Command/Tag/RenameTagCommandTest.php index 3a52aba3..4d647fe7 100644 --- a/module/CLI/test/Command/Tag/RenameTagCommandTest.php +++ b/module/CLI/test/Command/Tag/RenameTagCommandTest.php @@ -21,7 +21,7 @@ class RenameTagCommandTest extends TestCase private CommandTester $commandTester; private ObjectProphecy $tagService; - public function setUp(): void + protected function setUp(): void { $this->tagService = $this->prophesize(TagServiceInterface::class); $this->commandTester = $this->testerForCommand(new RenameTagCommand($this->tagService->reveal())); diff --git a/module/CLI/test/Command/Visit/LocateVisitsCommandTest.php b/module/CLI/test/Command/Visit/LocateVisitsCommandTest.php index fa666516..4111c1dc 100644 --- a/module/CLI/test/Command/Visit/LocateVisitsCommandTest.php +++ b/module/CLI/test/Command/Visit/LocateVisitsCommandTest.php @@ -40,7 +40,7 @@ class LocateVisitsCommandTest extends TestCase private ObjectProphecy $lock; private ObjectProphecy $downloadDbCommand; - public function setUp(): void + protected function setUp(): void { $this->visitService = $this->prophesize(VisitLocator::class); $this->ipResolver = $this->prophesize(IpLocationResolverInterface::class); diff --git a/module/CLI/test/ConfigProviderTest.php b/module/CLI/test/ConfigProviderTest.php index 863b8a1f..f01649e0 100644 --- a/module/CLI/test/ConfigProviderTest.php +++ b/module/CLI/test/ConfigProviderTest.php @@ -12,7 +12,7 @@ class ConfigProviderTest extends TestCase { private ConfigProvider $configProvider; - public function setUp(): void + protected function setUp(): void { $this->configProvider = new ConfigProvider(); } diff --git a/module/CLI/test/Factory/ApplicationFactoryTest.php b/module/CLI/test/Factory/ApplicationFactoryTest.php index fbb5ace9..cb08a692 100644 --- a/module/CLI/test/Factory/ApplicationFactoryTest.php +++ b/module/CLI/test/Factory/ApplicationFactoryTest.php @@ -16,7 +16,7 @@ class ApplicationFactoryTest extends TestCase private ApplicationFactory $factory; - public function setUp(): void + protected function setUp(): void { $this->factory = new ApplicationFactory(); } diff --git a/module/CLI/test/Util/GeolocationDbUpdaterTest.php b/module/CLI/test/Util/GeolocationDbUpdaterTest.php index c5e3bdb4..fde39775 100644 --- a/module/CLI/test/Util/GeolocationDbUpdaterTest.php +++ b/module/CLI/test/Util/GeolocationDbUpdaterTest.php @@ -32,7 +32,7 @@ class GeolocationDbUpdaterTest extends TestCase private TrackingOptions $trackingOptions; private ObjectProphecy $lock; - public function setUp(): void + protected function setUp(): void { $this->dbUpdater = $this->prophesize(DbUpdaterInterface::class); $this->geoLiteDbReader = $this->prophesize(Reader::class); diff --git a/module/CLI/test/Util/ShlinkTableTest.php b/module/CLI/test/Util/ShlinkTableTest.php index 1ca612d4..ffe1f30d 100644 --- a/module/CLI/test/Util/ShlinkTableTest.php +++ b/module/CLI/test/Util/ShlinkTableTest.php @@ -21,7 +21,7 @@ class ShlinkTableTest extends TestCase private ShlinkTable $shlinkTable; private ObjectProphecy $baseTable; - public function setUp(): void + protected function setUp(): void { $this->baseTable = $this->prophesize(Table::class); $this->shlinkTable = ShlinkTable::fromBaseTable($this->baseTable->reveal()); diff --git a/module/Core/test/Action/PixelActionTest.php b/module/Core/test/Action/PixelActionTest.php index fdd291a5..2972d4fd 100644 --- a/module/Core/test/Action/PixelActionTest.php +++ b/module/Core/test/Action/PixelActionTest.php @@ -25,7 +25,7 @@ class PixelActionTest extends TestCase private ObjectProphecy $urlResolver; private ObjectProphecy $requestTracker; - public function setUp(): void + protected function setUp(): void { $this->urlResolver = $this->prophesize(ShortUrlResolverInterface::class); $this->requestTracker = $this->prophesize(RequestTrackerInterface::class); diff --git a/module/Core/test/Action/QrCodeActionTest.php b/module/Core/test/Action/QrCodeActionTest.php index fb9e4e6a..1962fdc7 100644 --- a/module/Core/test/Action/QrCodeActionTest.php +++ b/module/Core/test/Action/QrCodeActionTest.php @@ -39,7 +39,7 @@ class QrCodeActionTest extends TestCase private ObjectProphecy $urlResolver; private QrCodeOptions $options; - public function setUp(): void + protected function setUp(): void { $router = $this->prophesize(RouterInterface::class); $router->generateUri(Argument::cetera())->willReturn('/foo/bar'); diff --git a/module/Core/test/Action/RedirectActionTest.php b/module/Core/test/Action/RedirectActionTest.php index cde2b9aa..aa2c9d07 100644 --- a/module/Core/test/Action/RedirectActionTest.php +++ b/module/Core/test/Action/RedirectActionTest.php @@ -31,7 +31,7 @@ class RedirectActionTest extends TestCase private ObjectProphecy $requestTracker; private ObjectProphecy $redirectRespHelper; - public function setUp(): void + protected function setUp(): void { $this->urlResolver = $this->prophesize(ShortUrlResolverInterface::class); $this->requestTracker = $this->prophesize(RequestTrackerInterface::class); diff --git a/module/Core/test/Config/BasePathPrefixerTest.php b/module/Core/test/Config/BasePathPrefixerTest.php index 36b038c8..2298a59c 100644 --- a/module/Core/test/Config/BasePathPrefixerTest.php +++ b/module/Core/test/Config/BasePathPrefixerTest.php @@ -11,7 +11,7 @@ class BasePathPrefixerTest extends TestCase { private BasePathPrefixer $prefixer; - public function setUp(): void + protected function setUp(): void { $this->prefixer = new BasePathPrefixer(); } diff --git a/module/Core/test/ConfigProviderTest.php b/module/Core/test/ConfigProviderTest.php index 33714f88..3bb0dbb4 100644 --- a/module/Core/test/ConfigProviderTest.php +++ b/module/Core/test/ConfigProviderTest.php @@ -12,7 +12,7 @@ class ConfigProviderTest extends TestCase { private ConfigProvider $configProvider; - public function setUp(): void + protected function setUp(): void { $this->configProvider = new ConfigProvider(); } diff --git a/module/Core/test/Domain/DomainServiceTest.php b/module/Core/test/Domain/DomainServiceTest.php index ea3cfe02..43a052ae 100644 --- a/module/Core/test/Domain/DomainServiceTest.php +++ b/module/Core/test/Domain/DomainServiceTest.php @@ -27,7 +27,7 @@ class DomainServiceTest extends TestCase private DomainService $domainService; private ObjectProphecy $em; - public function setUp(): void + protected function setUp(): void { $this->em = $this->prophesize(EntityManagerInterface::class); $this->domainService = new DomainService($this->em->reveal(), 'default.com'); diff --git a/module/Core/test/ErrorHandler/NotFoundRedirectHandlerTest.php b/module/Core/test/ErrorHandler/NotFoundRedirectHandlerTest.php index 70063764..d40fce56 100644 --- a/module/Core/test/ErrorHandler/NotFoundRedirectHandlerTest.php +++ b/module/Core/test/ErrorHandler/NotFoundRedirectHandlerTest.php @@ -31,7 +31,7 @@ class NotFoundRedirectHandlerTest extends TestCase private ObjectProphecy $next; private ServerRequestInterface $req; - public function setUp(): void + protected function setUp(): void { $this->redirectOptions = new NotFoundRedirectOptions(); $this->resolver = $this->prophesize(NotFoundRedirectResolverInterface::class); diff --git a/module/Core/test/ErrorHandler/NotFoundTemplateHandlerTest.php b/module/Core/test/ErrorHandler/NotFoundTemplateHandlerTest.php index dcf42b54..12865171 100644 --- a/module/Core/test/ErrorHandler/NotFoundTemplateHandlerTest.php +++ b/module/Core/test/ErrorHandler/NotFoundTemplateHandlerTest.php @@ -21,7 +21,7 @@ class NotFoundTemplateHandlerTest extends TestCase private NotFoundTemplateHandler $handler; private bool $readFileCalled; - public function setUp(): void + protected function setUp(): void { $this->readFileCalled = false; $readFile = function (string $fileName): string { diff --git a/module/Core/test/EventDispatcher/CloseDbConnectionEventListenerDelegatorTest.php b/module/Core/test/EventDispatcher/CloseDbConnectionEventListenerDelegatorTest.php index c928200e..b826802b 100644 --- a/module/Core/test/EventDispatcher/CloseDbConnectionEventListenerDelegatorTest.php +++ b/module/Core/test/EventDispatcher/CloseDbConnectionEventListenerDelegatorTest.php @@ -18,7 +18,7 @@ class CloseDbConnectionEventListenerDelegatorTest extends TestCase private CloseDbConnectionEventListenerDelegator $delegator; private ObjectProphecy $container; - public function setUp(): void + protected function setUp(): void { $this->container = $this->prophesize(ContainerInterface::class); $this->delegator = new CloseDbConnectionEventListenerDelegator(); diff --git a/module/Core/test/EventDispatcher/CloseDbConnectionEventListenerTest.php b/module/Core/test/EventDispatcher/CloseDbConnectionEventListenerTest.php index d0c7c374..7c4d74c8 100644 --- a/module/Core/test/EventDispatcher/CloseDbConnectionEventListenerTest.php +++ b/module/Core/test/EventDispatcher/CloseDbConnectionEventListenerTest.php @@ -20,7 +20,7 @@ class CloseDbConnectionEventListenerTest extends TestCase private ObjectProphecy $em; - public function setUp(): void + protected function setUp(): void { $this->em = $this->prophesize(ReopeningEntityManagerInterface::class); } diff --git a/module/Core/test/EventDispatcher/LocateVisitTest.php b/module/Core/test/EventDispatcher/LocateVisitTest.php index 09a8086d..5cf243d0 100644 --- a/module/Core/test/EventDispatcher/LocateVisitTest.php +++ b/module/Core/test/EventDispatcher/LocateVisitTest.php @@ -36,7 +36,7 @@ class LocateVisitTest extends TestCase private ObjectProphecy $dbUpdater; private ObjectProphecy $eventDispatcher; - public function setUp(): void + protected function setUp(): void { $this->ipLocationResolver = $this->prophesize(IpLocationResolverInterface::class); $this->em = $this->prophesize(EntityManagerInterface::class); diff --git a/module/Core/test/EventDispatcher/Mercure/NotifyVisitToMercureTest.php b/module/Core/test/EventDispatcher/Mercure/NotifyVisitToMercureTest.php index 1ce29d0d..0d8d9cfa 100644 --- a/module/Core/test/EventDispatcher/Mercure/NotifyVisitToMercureTest.php +++ b/module/Core/test/EventDispatcher/Mercure/NotifyVisitToMercureTest.php @@ -31,7 +31,7 @@ class NotifyVisitToMercureTest extends TestCase private ObjectProphecy $em; private ObjectProphecy $logger; - public function setUp(): void + protected function setUp(): void { $this->helper = $this->prophesize(PublishingHelperInterface::class); $this->updatesGenerator = $this->prophesize(PublishingUpdatesGeneratorInterface::class); diff --git a/module/Core/test/EventDispatcher/NotifyVisitToWebHooksTest.php b/module/Core/test/EventDispatcher/NotifyVisitToWebHooksTest.php index 56324e40..6be8719a 100644 --- a/module/Core/test/EventDispatcher/NotifyVisitToWebHooksTest.php +++ b/module/Core/test/EventDispatcher/NotifyVisitToWebHooksTest.php @@ -38,7 +38,7 @@ class NotifyVisitToWebHooksTest extends TestCase private ObjectProphecy $em; private ObjectProphecy $logger; - public function setUp(): void + protected function setUp(): void { $this->httpClient = $this->prophesize(ClientInterface::class); $this->em = $this->prophesize(EntityManagerInterface::class); diff --git a/module/Core/test/EventDispatcher/PublishingUpdatesGeneratorTest.php b/module/Core/test/EventDispatcher/PublishingUpdatesGeneratorTest.php index e4b616e8..5638b0fe 100644 --- a/module/Core/test/EventDispatcher/PublishingUpdatesGeneratorTest.php +++ b/module/Core/test/EventDispatcher/PublishingUpdatesGeneratorTest.php @@ -21,7 +21,7 @@ class PublishingUpdatesGeneratorTest extends TestCase { private PublishingUpdatesGenerator $generator; - public function setUp(): void + protected function setUp(): void { $this->generator = new PublishingUpdatesGenerator( new ShortUrlDataTransformer(new ShortUrlStringifier([])), diff --git a/module/Core/test/Service/ShortUrl/DeleteShortUrlServiceTest.php b/module/Core/test/Service/ShortUrl/DeleteShortUrlServiceTest.php index cd4d6193..391d52fd 100644 --- a/module/Core/test/Service/ShortUrl/DeleteShortUrlServiceTest.php +++ b/module/Core/test/Service/ShortUrl/DeleteShortUrlServiceTest.php @@ -31,7 +31,7 @@ class DeleteShortUrlServiceTest extends TestCase private ObjectProphecy $urlResolver; private string $shortCode; - public function setUp(): void + protected function setUp(): void { $shortUrl = ShortUrl::createEmpty()->setVisits(new ArrayCollection( map(range(0, 10), fn () => Visit::forValidShortUrl(ShortUrl::createEmpty(), Visitor::emptyInstance())), diff --git a/module/Core/test/Service/ShortUrl/ShortUrlResolverTest.php b/module/Core/test/Service/ShortUrl/ShortUrlResolverTest.php index bdccfa3f..d2c3bda5 100644 --- a/module/Core/test/Service/ShortUrl/ShortUrlResolverTest.php +++ b/module/Core/test/Service/ShortUrl/ShortUrlResolverTest.php @@ -32,7 +32,7 @@ class ShortUrlResolverTest extends TestCase private ShortUrlResolver $urlResolver; private ObjectProphecy $em; - public function setUp(): void + protected function setUp(): void { $this->em = $this->prophesize(EntityManagerInterface::class); $this->urlResolver = new ShortUrlResolver($this->em->reveal()); diff --git a/module/Core/test/Service/ShortUrlServiceTest.php b/module/Core/test/Service/ShortUrlServiceTest.php index 90000423..a042dd1f 100644 --- a/module/Core/test/Service/ShortUrlServiceTest.php +++ b/module/Core/test/Service/ShortUrlServiceTest.php @@ -34,7 +34,7 @@ class ShortUrlServiceTest extends TestCase private ObjectProphecy $urlResolver; private ObjectProphecy $titleResolutionHelper; - public function setUp(): void + protected function setUp(): void { $this->em = $this->prophesize(EntityManagerInterface::class); $this->em->persist(Argument::any())->willReturn(null); diff --git a/module/Core/test/Service/UrlShortenerTest.php b/module/Core/test/Service/UrlShortenerTest.php index fbe9b1c4..86a057e5 100644 --- a/module/Core/test/Service/UrlShortenerTest.php +++ b/module/Core/test/Service/UrlShortenerTest.php @@ -30,7 +30,7 @@ class UrlShortenerTest extends TestCase private ObjectProphecy $shortCodeHelper; private ObjectProphecy $eventDispatcher; - public function setUp(): void + protected function setUp(): void { $this->titleResolutionHelper = $this->prophesize(ShortUrlTitleResolutionHelperInterface::class); $this->titleResolutionHelper->processTitleAndValidateUrl(Argument::cetera())->willReturnArgument(); diff --git a/module/Core/test/ShortUrl/Paginator/Adapter/ShortUrlRepositoryAdapterTest.php b/module/Core/test/ShortUrl/Paginator/Adapter/ShortUrlRepositoryAdapterTest.php index 2675b04a..2abc08c3 100644 --- a/module/Core/test/ShortUrl/Paginator/Adapter/ShortUrlRepositoryAdapterTest.php +++ b/module/Core/test/ShortUrl/Paginator/Adapter/ShortUrlRepositoryAdapterTest.php @@ -22,7 +22,7 @@ class ShortUrlRepositoryAdapterTest extends TestCase private ObjectProphecy $repo; - public function setUp(): void + protected function setUp(): void { $this->repo = $this->prophesize(ShortUrlRepositoryInterface::class); } diff --git a/module/Core/test/ShortUrl/Resolver/PersistenceShortUrlRelationResolverTest.php b/module/Core/test/ShortUrl/Resolver/PersistenceShortUrlRelationResolverTest.php index 9aaf9495..39bb1b3f 100644 --- a/module/Core/test/ShortUrl/Resolver/PersistenceShortUrlRelationResolverTest.php +++ b/module/Core/test/ShortUrl/Resolver/PersistenceShortUrlRelationResolverTest.php @@ -25,7 +25,7 @@ class PersistenceShortUrlRelationResolverTest extends TestCase private PersistenceShortUrlRelationResolver $resolver; private ObjectProphecy $em; - public function setUp(): void + protected function setUp(): void { $this->em = $this->prophesize(EntityManagerInterface::class); $this->em->getEventManager()->willReturn(new EventManager()); diff --git a/module/Core/test/ShortUrl/Resolver/SimpleShortUrlRelationResolverTest.php b/module/Core/test/ShortUrl/Resolver/SimpleShortUrlRelationResolverTest.php index 483cb67a..669fdd6e 100644 --- a/module/Core/test/ShortUrl/Resolver/SimpleShortUrlRelationResolverTest.php +++ b/module/Core/test/ShortUrl/Resolver/SimpleShortUrlRelationResolverTest.php @@ -13,7 +13,7 @@ class SimpleShortUrlRelationResolverTest extends TestCase { private SimpleShortUrlRelationResolver $resolver; - public function setUp(): void + protected function setUp(): void { $this->resolver = new SimpleShortUrlRelationResolver(); } diff --git a/module/Core/test/ShortUrl/Transformer/ShortUrlDataTransformerTest.php b/module/Core/test/ShortUrl/Transformer/ShortUrlDataTransformerTest.php index 81c0d203..b48cd839 100644 --- a/module/Core/test/ShortUrl/Transformer/ShortUrlDataTransformerTest.php +++ b/module/Core/test/ShortUrl/Transformer/ShortUrlDataTransformerTest.php @@ -17,7 +17,7 @@ class ShortUrlDataTransformerTest extends TestCase { private ShortUrlDataTransformer $transformer; - public function setUp(): void + protected function setUp(): void { $this->transformer = new ShortUrlDataTransformer(new ShortUrlStringifier([])); } diff --git a/module/Core/test/Tag/TagServiceTest.php b/module/Core/test/Tag/TagServiceTest.php index 8c301f0f..d3e1b841 100644 --- a/module/Core/test/Tag/TagServiceTest.php +++ b/module/Core/test/Tag/TagServiceTest.php @@ -33,7 +33,7 @@ class TagServiceTest extends TestCase private ObjectProphecy $em; private ObjectProphecy $repo; - public function setUp(): void + protected function setUp(): void { $this->em = $this->prophesize(EntityManagerInterface::class); $this->repo = $this->prophesize(TagRepository::class); diff --git a/module/Core/test/Util/UrlValidatorTest.php b/module/Core/test/Util/UrlValidatorTest.php index 8aba6598..de5cad23 100644 --- a/module/Core/test/Util/UrlValidatorTest.php +++ b/module/Core/test/Util/UrlValidatorTest.php @@ -27,7 +27,7 @@ class UrlValidatorTest extends TestCase private ObjectProphecy $httpClient; private UrlShortenerOptions $options; - public function setUp(): void + protected function setUp(): void { $this->httpClient = $this->prophesize(ClientInterface::class); $this->options = new UrlShortenerOptions(['validate_url' => true]); diff --git a/module/Core/test/Visit/VisitLocatorTest.php b/module/Core/test/Visit/VisitLocatorTest.php index 5c51b848..b740d143 100644 --- a/module/Core/test/Visit/VisitLocatorTest.php +++ b/module/Core/test/Visit/VisitLocatorTest.php @@ -38,7 +38,7 @@ class VisitLocatorTest extends TestCase private ObjectProphecy $em; private ObjectProphecy $repo; - public function setUp(): void + protected function setUp(): void { $this->em = $this->prophesize(EntityManager::class); $this->repo = $this->prophesize(VisitRepositoryInterface::class); diff --git a/module/Core/test/Visit/VisitsStatsHelperTest.php b/module/Core/test/Visit/VisitsStatsHelperTest.php index 42c821bb..47288cb3 100644 --- a/module/Core/test/Visit/VisitsStatsHelperTest.php +++ b/module/Core/test/Visit/VisitsStatsHelperTest.php @@ -43,7 +43,7 @@ class VisitsStatsHelperTest extends TestCase private VisitsStatsHelper $helper; private ObjectProphecy $em; - public function setUp(): void + protected function setUp(): void { $this->em = $this->prophesize(EntityManagerInterface::class); $this->helper = new VisitsStatsHelper($this->em->reveal()); diff --git a/module/Core/test/Visit/VisitsTrackerTest.php b/module/Core/test/Visit/VisitsTrackerTest.php index 904f92d1..2bb13220 100644 --- a/module/Core/test/Visit/VisitsTrackerTest.php +++ b/module/Core/test/Visit/VisitsTrackerTest.php @@ -26,7 +26,7 @@ class VisitsTrackerTest extends TestCase private ObjectProphecy $eventDispatcher; private TrackingOptions $options; - public function setUp(): void + protected function setUp(): void { $this->em = $this->prophesize(EntityManager::class); diff --git a/module/Rest/test/Action/Domain/ListDomainsActionTest.php b/module/Rest/test/Action/Domain/ListDomainsActionTest.php index bc852b34..a6903b46 100644 --- a/module/Rest/test/Action/Domain/ListDomainsActionTest.php +++ b/module/Rest/test/Action/Domain/ListDomainsActionTest.php @@ -25,7 +25,7 @@ class ListDomainsActionTest extends TestCase private ObjectProphecy $domainService; private NotFoundRedirectOptions $options; - public function setUp(): void + protected function setUp(): void { $this->domainService = $this->prophesize(DomainServiceInterface::class); $this->options = new NotFoundRedirectOptions(); diff --git a/module/Rest/test/Action/HealthActionTest.php b/module/Rest/test/Action/HealthActionTest.php index a233087a..8298b2d1 100644 --- a/module/Rest/test/Action/HealthActionTest.php +++ b/module/Rest/test/Action/HealthActionTest.php @@ -25,7 +25,7 @@ class HealthActionTest extends TestCase private HealthAction $action; private ObjectProphecy $conn; - public function setUp(): void + protected function setUp(): void { $this->conn = $this->prophesize(Connection::class); $this->conn->executeQuery(Argument::cetera())->willReturn($this->prophesize(Result::class)->reveal()); diff --git a/module/Rest/test/Action/MercureInfoActionTest.php b/module/Rest/test/Action/MercureInfoActionTest.php index 33083c79..e586a641 100644 --- a/module/Rest/test/Action/MercureInfoActionTest.php +++ b/module/Rest/test/Action/MercureInfoActionTest.php @@ -21,7 +21,7 @@ class MercureInfoActionTest extends TestCase private ObjectProphecy $provider; - public function setUp(): void + protected function setUp(): void { $this->provider = $this->prophesize(JwtProviderInterface::class); } diff --git a/module/Rest/test/Action/ShortUrl/CreateShortUrlActionTest.php b/module/Rest/test/Action/ShortUrl/CreateShortUrlActionTest.php index 206b016f..eb0d8622 100644 --- a/module/Rest/test/Action/ShortUrl/CreateShortUrlActionTest.php +++ b/module/Rest/test/Action/ShortUrl/CreateShortUrlActionTest.php @@ -29,7 +29,7 @@ class CreateShortUrlActionTest extends TestCase private ObjectProphecy $urlShortener; private ObjectProphecy $transformer; - public function setUp(): void + protected function setUp(): void { $this->urlShortener = $this->prophesize(UrlShortener::class); $this->transformer = $this->prophesize(DataTransformerInterface::class); diff --git a/module/Rest/test/Action/ShortUrl/DeleteShortUrlActionTest.php b/module/Rest/test/Action/ShortUrl/DeleteShortUrlActionTest.php index 9705cd59..ae49cf4b 100644 --- a/module/Rest/test/Action/ShortUrl/DeleteShortUrlActionTest.php +++ b/module/Rest/test/Action/ShortUrl/DeleteShortUrlActionTest.php @@ -20,7 +20,7 @@ class DeleteShortUrlActionTest extends TestCase private DeleteShortUrlAction $action; private ObjectProphecy $service; - public function setUp(): void + protected function setUp(): void { $this->service = $this->prophesize(DeleteShortUrlServiceInterface::class); $this->action = new DeleteShortUrlAction($this->service->reveal()); diff --git a/module/Rest/test/Action/ShortUrl/EditShortUrlActionTest.php b/module/Rest/test/Action/ShortUrl/EditShortUrlActionTest.php index e1f434df..4d09042d 100644 --- a/module/Rest/test/Action/ShortUrl/EditShortUrlActionTest.php +++ b/module/Rest/test/Action/ShortUrl/EditShortUrlActionTest.php @@ -24,7 +24,7 @@ class EditShortUrlActionTest extends TestCase private EditShortUrlAction $action; private ObjectProphecy $shortUrlService; - public function setUp(): void + protected function setUp(): void { $this->shortUrlService = $this->prophesize(ShortUrlServiceInterface::class); $this->action = new EditShortUrlAction($this->shortUrlService->reveal(), new ShortUrlDataTransformer( diff --git a/module/Rest/test/Action/ShortUrl/ListShortUrlsActionTest.php b/module/Rest/test/Action/ShortUrl/ListShortUrlsActionTest.php index 59876b55..8b295358 100644 --- a/module/Rest/test/Action/ShortUrl/ListShortUrlsActionTest.php +++ b/module/Rest/test/Action/ShortUrl/ListShortUrlsActionTest.php @@ -26,7 +26,7 @@ class ListShortUrlsActionTest extends TestCase private ListShortUrlsAction $action; private ObjectProphecy $service; - public function setUp(): void + protected function setUp(): void { $this->service = $this->prophesize(ShortUrlService::class); diff --git a/module/Rest/test/Action/ShortUrl/ResolveShortUrlActionTest.php b/module/Rest/test/Action/ShortUrl/ResolveShortUrlActionTest.php index 19422d9d..78898f7a 100644 --- a/module/Rest/test/Action/ShortUrl/ResolveShortUrlActionTest.php +++ b/module/Rest/test/Action/ShortUrl/ResolveShortUrlActionTest.php @@ -23,7 +23,7 @@ class ResolveShortUrlActionTest extends TestCase private ResolveShortUrlAction $action; private ObjectProphecy $urlResolver; - public function setUp(): void + protected function setUp(): void { $this->urlResolver = $this->prophesize(ShortUrlResolverInterface::class); $this->action = new ResolveShortUrlAction($this->urlResolver->reveal(), new ShortUrlDataTransformer( diff --git a/module/Rest/test/Action/ShortUrl/SingleStepCreateShortUrlActionTest.php b/module/Rest/test/Action/ShortUrl/SingleStepCreateShortUrlActionTest.php index e3fd3e10..f62a5da6 100644 --- a/module/Rest/test/Action/ShortUrl/SingleStepCreateShortUrlActionTest.php +++ b/module/Rest/test/Action/ShortUrl/SingleStepCreateShortUrlActionTest.php @@ -25,7 +25,7 @@ class SingleStepCreateShortUrlActionTest extends TestCase private ObjectProphecy $urlShortener; private ObjectProphecy $transformer; - public function setUp(): void + protected function setUp(): void { $this->urlShortener = $this->prophesize(UrlShortenerInterface::class); $this->transformer = $this->prophesize(DataTransformerInterface::class); diff --git a/module/Rest/test/Action/Tag/DeleteTagsActionTest.php b/module/Rest/test/Action/Tag/DeleteTagsActionTest.php index 4812649d..457507e8 100644 --- a/module/Rest/test/Action/Tag/DeleteTagsActionTest.php +++ b/module/Rest/test/Action/Tag/DeleteTagsActionTest.php @@ -20,7 +20,7 @@ class DeleteTagsActionTest extends TestCase private DeleteTagsAction $action; private ObjectProphecy $tagService; - public function setUp(): void + protected function setUp(): void { $this->tagService = $this->prophesize(TagServiceInterface::class); $this->action = new DeleteTagsAction($this->tagService->reveal()); diff --git a/module/Rest/test/Action/Tag/ListTagsActionTest.php b/module/Rest/test/Action/Tag/ListTagsActionTest.php index 123e4945..3da8594c 100644 --- a/module/Rest/test/Action/Tag/ListTagsActionTest.php +++ b/module/Rest/test/Action/Tag/ListTagsActionTest.php @@ -28,7 +28,7 @@ class ListTagsActionTest extends TestCase private ListTagsAction $action; private ObjectProphecy $tagService; - public function setUp(): void + protected function setUp(): void { $this->tagService = $this->prophesize(TagServiceInterface::class); $this->action = new ListTagsAction($this->tagService->reveal()); diff --git a/module/Rest/test/Action/Tag/TagsStatsActionTest.php b/module/Rest/test/Action/Tag/TagsStatsActionTest.php index 2cb3ad64..44e6afb0 100644 --- a/module/Rest/test/Action/Tag/TagsStatsActionTest.php +++ b/module/Rest/test/Action/Tag/TagsStatsActionTest.php @@ -27,7 +27,7 @@ class TagsStatsActionTest extends TestCase private TagsStatsAction $action; private ObjectProphecy $tagService; - public function setUp(): void + protected function setUp(): void { $this->tagService = $this->prophesize(TagServiceInterface::class); $this->action = new TagsStatsAction($this->tagService->reveal()); diff --git a/module/Rest/test/Action/Tag/UpdateTagActionTest.php b/module/Rest/test/Action/Tag/UpdateTagActionTest.php index d7b398db..a3bce658 100644 --- a/module/Rest/test/Action/Tag/UpdateTagActionTest.php +++ b/module/Rest/test/Action/Tag/UpdateTagActionTest.php @@ -24,7 +24,7 @@ class UpdateTagActionTest extends TestCase private UpdateTagAction $action; private ObjectProphecy $tagService; - public function setUp(): void + protected function setUp(): void { $this->tagService = $this->prophesize(TagServiceInterface::class); $this->action = new UpdateTagAction($this->tagService->reveal()); diff --git a/module/Rest/test/Action/Visit/GlobalVisitsActionTest.php b/module/Rest/test/Action/Visit/GlobalVisitsActionTest.php index 829b820b..d5f94250 100644 --- a/module/Rest/test/Action/Visit/GlobalVisitsActionTest.php +++ b/module/Rest/test/Action/Visit/GlobalVisitsActionTest.php @@ -21,7 +21,7 @@ class GlobalVisitsActionTest extends TestCase private GlobalVisitsAction $action; private ObjectProphecy $helper; - public function setUp(): void + protected function setUp(): void { $this->helper = $this->prophesize(VisitsStatsHelperInterface::class); $this->action = new GlobalVisitsAction($this->helper->reveal()); diff --git a/module/Rest/test/Action/Visit/NonOrphanVisitsActionTest.php b/module/Rest/test/Action/Visit/NonOrphanVisitsActionTest.php index 5b3487f0..60224bef 100644 --- a/module/Rest/test/Action/Visit/NonOrphanVisitsActionTest.php +++ b/module/Rest/test/Action/Visit/NonOrphanVisitsActionTest.php @@ -24,7 +24,7 @@ class NonOrphanVisitsActionTest extends TestCase private NonOrphanVisitsAction $action; private ObjectProphecy $visitsHelper; - public function setUp(): void + protected function setUp(): void { $this->visitsHelper = $this->prophesize(VisitsStatsHelperInterface::class); $this->action = new NonOrphanVisitsAction($this->visitsHelper->reveal()); diff --git a/module/Rest/test/Action/Visit/ShortUrlVisitsActionTest.php b/module/Rest/test/Action/Visit/ShortUrlVisitsActionTest.php index 299c42d1..d9f248e6 100644 --- a/module/Rest/test/Action/Visit/ShortUrlVisitsActionTest.php +++ b/module/Rest/test/Action/Visit/ShortUrlVisitsActionTest.php @@ -27,7 +27,7 @@ class ShortUrlVisitsActionTest extends TestCase private ShortUrlVisitsAction $action; private ObjectProphecy $visitsHelper; - public function setUp(): void + protected function setUp(): void { $this->visitsHelper = $this->prophesize(VisitsStatsHelperInterface::class); $this->action = new ShortUrlVisitsAction($this->visitsHelper->reveal()); diff --git a/module/Rest/test/ConfigProviderTest.php b/module/Rest/test/ConfigProviderTest.php index 06bf179f..1f7044f9 100644 --- a/module/Rest/test/ConfigProviderTest.php +++ b/module/Rest/test/ConfigProviderTest.php @@ -12,7 +12,7 @@ class ConfigProviderTest extends TestCase { private ConfigProvider $configProvider; - public function setUp(): void + protected function setUp(): void { $this->configProvider = new ConfigProvider(); } diff --git a/module/Rest/test/Middleware/AuthenticationMiddlewareTest.php b/module/Rest/test/Middleware/AuthenticationMiddlewareTest.php index c915098a..eef78ab7 100644 --- a/module/Rest/test/Middleware/AuthenticationMiddlewareTest.php +++ b/module/Rest/test/Middleware/AuthenticationMiddlewareTest.php @@ -35,7 +35,7 @@ class AuthenticationMiddlewareTest extends TestCase private ObjectProphecy $apiKeyService; private ObjectProphecy $handler; - public function setUp(): void + protected function setUp(): void { $this->apiKeyService = $this->prophesize(ApiKeyServiceInterface::class); $this->middleware = new AuthenticationMiddleware( diff --git a/module/Rest/test/Middleware/BodyParserMiddlewareTest.php b/module/Rest/test/Middleware/BodyParserMiddlewareTest.php index 04c9478d..f254197e 100644 --- a/module/Rest/test/Middleware/BodyParserMiddlewareTest.php +++ b/module/Rest/test/Middleware/BodyParserMiddlewareTest.php @@ -23,7 +23,7 @@ class BodyParserMiddlewareTest extends TestCase private BodyParserMiddleware $middleware; - public function setUp(): void + protected function setUp(): void { $this->middleware = new BodyParserMiddleware(); } diff --git a/module/Rest/test/Middleware/CrossDomainMiddlewareTest.php b/module/Rest/test/Middleware/CrossDomainMiddlewareTest.php index acdc9600..286652bf 100644 --- a/module/Rest/test/Middleware/CrossDomainMiddlewareTest.php +++ b/module/Rest/test/Middleware/CrossDomainMiddlewareTest.php @@ -20,7 +20,7 @@ class CrossDomainMiddlewareTest extends TestCase private CrossDomainMiddleware $middleware; private ObjectProphecy $handler; - public function setUp(): void + protected function setUp(): void { $this->middleware = new CrossDomainMiddleware(['max_age' => 1000]); $this->handler = $this->prophesize(RequestHandlerInterface::class); diff --git a/module/Rest/test/Middleware/EmptyResponseImplicitOptionsMiddlewareFactoryTest.php b/module/Rest/test/Middleware/EmptyResponseImplicitOptionsMiddlewareFactoryTest.php index 4928f2ef..b2093461 100644 --- a/module/Rest/test/Middleware/EmptyResponseImplicitOptionsMiddlewareFactoryTest.php +++ b/module/Rest/test/Middleware/EmptyResponseImplicitOptionsMiddlewareFactoryTest.php @@ -15,7 +15,7 @@ class EmptyResponseImplicitOptionsMiddlewareFactoryTest extends TestCase { private EmptyResponseImplicitOptionsMiddlewareFactory $factory; - public function setUp(): void + protected function setUp(): void { $this->factory = new EmptyResponseImplicitOptionsMiddlewareFactory(); } diff --git a/module/Rest/test/Middleware/ShortUrl/CreateShortUrlContentNegotiationMiddlewareTest.php b/module/Rest/test/Middleware/ShortUrl/CreateShortUrlContentNegotiationMiddlewareTest.php index dc4733ff..b77d79a9 100644 --- a/module/Rest/test/Middleware/ShortUrl/CreateShortUrlContentNegotiationMiddlewareTest.php +++ b/module/Rest/test/Middleware/ShortUrl/CreateShortUrlContentNegotiationMiddlewareTest.php @@ -22,7 +22,7 @@ class CreateShortUrlContentNegotiationMiddlewareTest extends TestCase private CreateShortUrlContentNegotiationMiddleware $middleware; private ObjectProphecy $requestHandler; - public function setUp(): void + protected function setUp(): void { $this->middleware = new CreateShortUrlContentNegotiationMiddleware(); $this->requestHandler = $this->prophesize(RequestHandlerInterface::class); diff --git a/module/Rest/test/Middleware/ShortUrl/DefaultShortCodesLengthMiddlewareTest.php b/module/Rest/test/Middleware/ShortUrl/DefaultShortCodesLengthMiddlewareTest.php index e10e9f73..2aef77b7 100644 --- a/module/Rest/test/Middleware/ShortUrl/DefaultShortCodesLengthMiddlewareTest.php +++ b/module/Rest/test/Middleware/ShortUrl/DefaultShortCodesLengthMiddlewareTest.php @@ -23,7 +23,7 @@ class DefaultShortCodesLengthMiddlewareTest extends TestCase private DefaultShortCodesLengthMiddleware $middleware; private ObjectProphecy $handler; - public function setUp(): void + protected function setUp(): void { $this->handler = $this->prophesize(RequestHandlerInterface::class); $this->middleware = new DefaultShortCodesLengthMiddleware(8); diff --git a/module/Rest/test/Middleware/ShortUrl/DropDefaultDomainFromRequestMiddlewareTest.php b/module/Rest/test/Middleware/ShortUrl/DropDefaultDomainFromRequestMiddlewareTest.php index 24f3aecd..9418a16a 100644 --- a/module/Rest/test/Middleware/ShortUrl/DropDefaultDomainFromRequestMiddlewareTest.php +++ b/module/Rest/test/Middleware/ShortUrl/DropDefaultDomainFromRequestMiddlewareTest.php @@ -22,7 +22,7 @@ class DropDefaultDomainFromRequestMiddlewareTest extends TestCase private DropDefaultDomainFromRequestMiddleware $middleware; private ObjectProphecy $next; - public function setUp(): void + protected function setUp(): void { $this->next = $this->prophesize(RequestHandlerInterface::class); $this->middleware = new DropDefaultDomainFromRequestMiddleware('doma.in'); diff --git a/module/Rest/test/Service/ApiKeyServiceTest.php b/module/Rest/test/Service/ApiKeyServiceTest.php index aba79036..f384a45a 100644 --- a/module/Rest/test/Service/ApiKeyServiceTest.php +++ b/module/Rest/test/Service/ApiKeyServiceTest.php @@ -25,7 +25,7 @@ class ApiKeyServiceTest extends TestCase private ApiKeyService $service; private ObjectProphecy $em; - public function setUp(): void + protected function setUp(): void { $this->em = $this->prophesize(EntityManager::class); $this->service = new ApiKeyService($this->em->reveal()); From 0e54ed691d2a5b6f81824e8289f15874eb13eaf5 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 11 Sep 2022 12:10:21 +0200 Subject: [PATCH 082/108] Created InitialApiKeyDelegatorTest --- module/Rest/config/initial-api-key.config.php | 2 + .../ApiKey/InitialApiKeyDelegatorTest.php | 61 +++++++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 module/Rest/test/ApiKey/InitialApiKeyDelegatorTest.php diff --git a/module/Rest/config/initial-api-key.config.php b/module/Rest/config/initial-api-key.config.php index 57114479..a44f877f 100644 --- a/module/Rest/config/initial-api-key.config.php +++ b/module/Rest/config/initial-api-key.config.php @@ -11,6 +11,8 @@ use const PHP_SAPI; return [ + // We will try to load the initial API key only for openswoole and RoadRunner. + // For php-fpm, the check against the database would happen on every request, resulting in a very bad performance. 'initial_api_key' => PHP_SAPI !== 'cli' ? null : EnvVars::INITIAL_API_KEY->loadFromEnv(), 'dependencies' => [ diff --git a/module/Rest/test/ApiKey/InitialApiKeyDelegatorTest.php b/module/Rest/test/ApiKey/InitialApiKeyDelegatorTest.php new file mode 100644 index 00000000..7614fc9d --- /dev/null +++ b/module/Rest/test/ApiKey/InitialApiKeyDelegatorTest.php @@ -0,0 +1,61 @@ +delegator = new InitialApiKeyDelegator(); + $this->container = $this->prophesize(ContainerInterface::class); + } + + /** + * @test + * @dataProvider provideConfigs + */ + public function apiKeyIsInitializedWhenAppropriate(array $config, int $expectedCalls): void + { + $app = $this->prophesize(Application::class)->reveal(); + $apiKeyRepo = $this->prophesize(ApiKeyRepositoryInterface::class); + $em = $this->prophesize(EntityManagerInterface::class); + + $getConfig = $this->container->get('config')->willReturn($config); + $getRepo = $em->getRepository(ApiKey::class)->willReturn($apiKeyRepo->reveal()); + $getEm = $this->container->get(EntityManager::class)->willReturn($em->reveal()); + + $result = ($this->delegator)($this->container->reveal(), '', fn () => $app); + + self::assertSame($result, $app); + $getConfig->shouldHaveBeenCalledOnce(); + $getRepo->shouldHaveBeenCalledTimes($expectedCalls); + $getEm->shouldHaveBeenCalledTimes($expectedCalls); + $apiKeyRepo->createInitialApiKey(Argument::any())->shouldHaveBeenCalledTimes($expectedCalls); + } + + public function provideConfigs(): iterable + { + yield [[], 0]; + yield [['initial_api_key' => null], 0]; + yield [['initial_api_key' => 'the_initial_key'], 1]; + } +} From eed7b6e565cc26516b87a84bff96066d20e69305 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 11 Sep 2022 12:18:04 +0200 Subject: [PATCH 083/108] Added db test for ApiKeyRepository --- composer.json | 1 + .../Repository/ApiKeyRepositoryTest.php | 31 +++++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 module/Rest/test-db/ApiKey/Repository/ApiKeyRepositoryTest.php diff --git a/composer.json b/composer.json index a9b60f12..159d64bd 100644 --- a/composer.json +++ b/composer.json @@ -92,6 +92,7 @@ "ShlinkioCliTest\\Shlink\\CLI\\": "module/CLI/test-cli", "ShlinkioTest\\Shlink\\Rest\\": "module/Rest/test", "ShlinkioApiTest\\Shlink\\Rest\\": "module/Rest/test-api", + "ShlinkioDbTest\\Shlink\\Rest\\": "module/Rest/test-db", "ShlinkioTest\\Shlink\\Core\\": "module/Core/test", "ShlinkioDbTest\\Shlink\\Core\\": "module/Core/test-db" }, diff --git a/module/Rest/test-db/ApiKey/Repository/ApiKeyRepositoryTest.php b/module/Rest/test-db/ApiKey/Repository/ApiKeyRepositoryTest.php new file mode 100644 index 00000000..ae6ab0a0 --- /dev/null +++ b/module/Rest/test-db/ApiKey/Repository/ApiKeyRepositoryTest.php @@ -0,0 +1,31 @@ +repo = $this->getEntityManager()->getRepository(ApiKey::class); + } + + /** @test */ + public function initialApiKeyIsCreatedOnlyOfNoApiKeysExistYet(): void + { + self::assertCount(0, $this->repo->findAll()); + $this->repo->createInitialApiKey('initial_value'); + self::assertCount(1, $this->repo->findAll()); + self::assertCount(1, $this->repo->findBy(['key' => 'initial_value'])); + $this->repo->createInitialApiKey('another_one'); + self::assertCount(1, $this->repo->findAll()); + self::assertCount(0, $this->repo->findBy(['key' => 'another_one'])); + } +} From 1966367caf4100a65ab56412387924c7c414ca4e Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 11 Sep 2022 12:33:28 +0200 Subject: [PATCH 084/108] Fixed ApiKeyRepository for MS and Postgres --- .../src/ApiKey/Repository/ApiKeyRepository.php | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/module/Rest/src/ApiKey/Repository/ApiKeyRepository.php b/module/Rest/src/ApiKey/Repository/ApiKeyRepository.php index 46c1e1a5..ec49145e 100644 --- a/module/Rest/src/ApiKey/Repository/ApiKeyRepository.php +++ b/module/Rest/src/ApiKey/Repository/ApiKeyRepository.php @@ -14,13 +14,16 @@ class ApiKeyRepository extends EntitySpecificationRepository implements ApiKeyRe { $em = $this->getEntityManager(); $em->wrapInTransaction(function () use ($apiKey, $em): void { - $amountOfApiKeys = $em->createQueryBuilder()->select('COUNT(a.id)') - ->from(ApiKey::class, 'a') - ->getQuery() - ->setLockMode(LockMode::PESSIMISTIC_WRITE) - ->getSingleScalarResult(); + // Ideally this would be a SELECT COUNT(...), but MsSQL and Postgres do not allow locking on aggregates + // Because of that we check if at least one result exists + $firstResult = $em->createQueryBuilder()->select('a.id') + ->from(ApiKey::class, 'a') + ->setMaxResults(1) + ->getQuery() + ->setLockMode(LockMode::PESSIMISTIC_WRITE) + ->getOneOrNullResult(); - if ($amountOfApiKeys === 0) { + if ($firstResult === null) { $em->persist(ApiKey::fromKey($apiKey)); $em->flush(); } From c5eda37bdae3d99465922cc5c1e1823ac86cbbd1 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 11 Sep 2022 12:36:47 +0200 Subject: [PATCH 085/108] Updated changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 921e45d4..7f858439 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this Non-error responses are not affected. * [#1513](https://github.com/shlinkio/shlink/issues/1513) Added publishing of the docker image in GHCR. +* [#1114](https://github.com/shlinkio/shlink/issues/1114) Added support to provide an initial API key via `INITIAL_API_KEY` env var, when running Shlink with openswoole or RoadRunner. + + Also, the installer tool now allows to generate an initial API key that can be copy-pasted (this tool is run interactively), in case you use php-fpm or you don't want to use env vars. ### Changed * [#1339](https://github.com/shlinkio/shlink/issues/1339) Added new test suite for CLI E2E tests. From da3ee6b65e94ecade309df1fc12a949285eae9e3 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 11 Sep 2022 13:14:36 +0200 Subject: [PATCH 086/108] Updated installer with support for API key generation --- composer.json | 2 +- config/autoload/installer.global.php | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 159d64bd..a5bc5b62 100644 --- a/composer.json +++ b/composer.json @@ -47,7 +47,7 @@ "shlinkio/shlink-config": "dev-main#33004e6 as 2.1", "shlinkio/shlink-event-dispatcher": "dev-main#48c0137 as 2.6", "shlinkio/shlink-importer": "^4.0", - "shlinkio/shlink-installer": "dev-develop#f1cc5c7 as 8.2", + "shlinkio/shlink-installer": "dev-develop#a01bca9 as 8.2", "shlinkio/shlink-ip-geolocation": "^3.0", "spiral/roadrunner": "^2.11", "spiral/roadrunner-jobs": "^2.3", diff --git a/config/autoload/installer.global.php b/config/autoload/installer.global.php index 40c2f4bd..fbc5fa03 100644 --- a/config/autoload/installer.global.php +++ b/config/autoload/installer.global.php @@ -82,6 +82,9 @@ return [ InstallationCommand::GEOLITE_DOWNLOAD_DB->value => [ 'command' => 'bin/cli ' . Command\Visit\DownloadGeoLiteDbCommand::NAME, ], + InstallationCommand::API_KEY_GENERATE->value => [ + 'command' => 'bin/cli ' . Command\Api\GenerateKeyCommand::NAME, + ], ], ], From 5f87bb13f8633cb46d86eba95b5443f7c801cca8 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 17 Sep 2022 11:27:56 +0200 Subject: [PATCH 087/108] Fixed tracking config --- config/autoload/tracking.global.php | 47 ++++++++++++++++------------- config/roadrunner/.rr.dev.yml | 4 +-- 2 files changed, 28 insertions(+), 23 deletions(-) diff --git a/config/autoload/tracking.global.php b/config/autoload/tracking.global.php index 0637301a..1fc05b3b 100644 --- a/config/autoload/tracking.global.php +++ b/config/autoload/tracking.global.php @@ -4,33 +4,38 @@ declare(strict_types=1); use Shlinkio\Shlink\Core\Config\EnvVars; -return [ +return (static function (): array { + /** @var string|null $disableTrackingFrom */ + $disableTrackingFrom = EnvVars::DISABLE_TRACKING_FROM->loadFromEnv(); - 'tracking' => [ - // Tells if IP addresses should be anonymized before persisting, to fulfil data protection regulations - // This applies only if IP address tracking is enabled - 'anonymize_remote_addr' => (bool) EnvVars::ANONYMIZE_REMOTE_ADDR->loadFromEnv(true), + return [ - // Tells if visits to not-found URLs should be tracked. The disable_tracking option takes precedence - 'track_orphan_visits' => (bool) EnvVars::TRACK_ORPHAN_VISITS->loadFromEnv(true), + 'tracking' => [ + // Tells if IP addresses should be anonymized before persisting, to fulfil data protection regulations + // This applies only if IP address tracking is enabled + 'anonymize_remote_addr' => (bool) EnvVars::ANONYMIZE_REMOTE_ADDR->loadFromEnv(true), - // A query param that, if provided, will disable tracking of one particular visit. Always takes precedence - 'disable_track_param' => EnvVars::DISABLE_TRACK_PARAM->loadFromEnv(), + // Tells if visits to not-found URLs should be tracked. The disable_tracking option takes precedence + 'track_orphan_visits' => (bool) EnvVars::TRACK_ORPHAN_VISITS->loadFromEnv(true), - // If true, visits will not be tracked at all - 'disable_tracking' => (bool) EnvVars::DISABLE_TRACKING->loadFromEnv(false), + // A query param that, if provided, will disable tracking of one particular visit. Always takes precedence + 'disable_track_param' => EnvVars::DISABLE_TRACK_PARAM->loadFromEnv(), - // If true, visits will be tracked, but neither the IP address, nor the location will be resolved - 'disable_ip_tracking' => (bool) EnvVars::DISABLE_IP_TRACKING->loadFromEnv(false), + // If true, visits will not be tracked at all + 'disable_tracking' => (bool) EnvVars::DISABLE_TRACKING->loadFromEnv(false), - // If true, the referrer will not be tracked - 'disable_referrer_tracking' => (bool) EnvVars::DISABLE_REFERRER_TRACKING->loadFromEnv(false), + // If true, visits will be tracked, but neither the IP address, nor the location will be resolved + 'disable_ip_tracking' => (bool) EnvVars::DISABLE_IP_TRACKING->loadFromEnv(false), - // If true, the user agent will not be tracked - 'disable_ua_tracking' => (bool) EnvVars::DISABLE_UA_TRACKING->loadFromEnv(false), + // If true, the referrer will not be tracked + 'disable_referrer_tracking' => (bool) EnvVars::DISABLE_REFERRER_TRACKING->loadFromEnv(false), - // A list of IP addresses, patterns or CIDR blocks from which tracking is disabled by default - 'disable_tracking_from' => EnvVars::DISABLE_TRACKING_FROM->loadFromEnv(), - ], + // If true, the user agent will not be tracked + 'disable_ua_tracking' => (bool) EnvVars::DISABLE_UA_TRACKING->loadFromEnv(false), -]; + // A list of IP addresses, patterns or CIDR blocks from which tracking is disabled by default + 'disable_tracking_from' => $disableTrackingFrom === null ? [] : explode(',', $disableTrackingFrom), + ], + + ]; +})(); diff --git a/config/roadrunner/.rr.dev.yml b/config/roadrunner/.rr.dev.yml index 7adf4520..cc0bbf29 100644 --- a/config/roadrunner/.rr.dev.yml +++ b/config/roadrunner/.rr.dev.yml @@ -13,11 +13,11 @@ http: dir: '../../public' forbid: ['.php', '.htaccess'] pool: - num_workers: 16 + num_workers: 1 jobs: pool: - num_workers: 16 + num_workers: 1 timeout: 300 consume: ['shlink'] pipelines: From fe4b2c4ae49f0bdc3242a71a9d67cc9e8a2ba9d3 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 17 Sep 2022 12:57:04 +0200 Subject: [PATCH 088/108] Migrated TrackingOptions to immutable object --- composer.json | 4 +- config/autoload/tracking.global.php | 4 +- module/CLI/src/Util/GeolocationDbUpdater.php | 2 +- .../test/Util/GeolocationDbUpdaterTest.php | 45 ++++--- module/Core/config/dependencies.config.php | 4 +- module/Core/src/Model/Visitor.php | 6 +- module/Core/src/Options/TrackingOptions.php | 114 +++--------------- .../Helper/ShortUrlRedirectionBuilder.php | 2 +- module/Core/src/Visit/RequestTracker.php | 2 +- module/Core/src/Visit/VisitsTracker.php | 12 +- module/Core/test/Model/VisitorTest.php | 10 +- .../Helper/ShortUrlRedirectionBuilderTest.php | 2 +- module/Core/test/Visit/RequestTrackerTest.php | 8 +- module/Core/test/Visit/VisitsTrackerTest.php | 24 ++-- 14 files changed, 77 insertions(+), 162 deletions(-) diff --git a/composer.json b/composer.json index a5bc5b62..8a53f16c 100644 --- a/composer.json +++ b/composer.json @@ -43,8 +43,8 @@ "php-middleware/request-id": "^4.1", "pugx/shortid-php": "^1.0", "ramsey/uuid": "^4.3", - "shlinkio/shlink-common": "^5.0", - "shlinkio/shlink-config": "dev-main#33004e6 as 2.1", + "shlinkio/shlink-common": "dev-main#c9e6474 as 5.1", + "shlinkio/shlink-config": "dev-main#12fb295 as 2.1", "shlinkio/shlink-event-dispatcher": "dev-main#48c0137 as 2.6", "shlinkio/shlink-importer": "^4.0", "shlinkio/shlink-installer": "dev-develop#a01bca9 as 8.2", diff --git a/config/autoload/tracking.global.php b/config/autoload/tracking.global.php index 1fc05b3b..4d7a6e9a 100644 --- a/config/autoload/tracking.global.php +++ b/config/autoload/tracking.global.php @@ -34,7 +34,9 @@ return (static function (): array { 'disable_ua_tracking' => (bool) EnvVars::DISABLE_UA_TRACKING->loadFromEnv(false), // A list of IP addresses, patterns or CIDR blocks from which tracking is disabled by default - 'disable_tracking_from' => $disableTrackingFrom === null ? [] : explode(',', $disableTrackingFrom), + 'disable_tracking_from' => $disableTrackingFrom === null + ? [] + : array_map(trim(...), explode(',', $disableTrackingFrom)), ], ]; diff --git a/module/CLI/src/Util/GeolocationDbUpdater.php b/module/CLI/src/Util/GeolocationDbUpdater.php index 22a3bac5..913ad438 100644 --- a/module/CLI/src/Util/GeolocationDbUpdater.php +++ b/module/CLI/src/Util/GeolocationDbUpdater.php @@ -32,7 +32,7 @@ class GeolocationDbUpdater implements GeolocationDbUpdaterInterface */ public function checkDbUpdate(?callable $beforeDownload = null, ?callable $handleProgress = null): void { - if ($this->trackingOptions->disableTracking() || $this->trackingOptions->disableIpTracking()) { + if ($this->trackingOptions->disableTracking || $this->trackingOptions->disableIpTracking) { return; } diff --git a/module/CLI/test/Util/GeolocationDbUpdaterTest.php b/module/CLI/test/Util/GeolocationDbUpdaterTest.php index fde39775..a884dd7c 100644 --- a/module/CLI/test/Util/GeolocationDbUpdaterTest.php +++ b/module/CLI/test/Util/GeolocationDbUpdaterTest.php @@ -29,7 +29,6 @@ class GeolocationDbUpdaterTest extends TestCase private GeolocationDbUpdater $geolocationDbUpdater; private ObjectProphecy $dbUpdater; private ObjectProphecy $geoLiteDbReader; - private TrackingOptions $trackingOptions; private ObjectProphecy $lock; protected function setUp(): void @@ -38,19 +37,10 @@ class GeolocationDbUpdaterTest extends TestCase $this->geoLiteDbReader = $this->prophesize(Reader::class); $this->trackingOptions = new TrackingOptions(); - $locker = $this->prophesize(Lock\LockFactory::class); $this->lock = $this->prophesize(Lock\LockInterface::class); $this->lock->acquire(true)->willReturn(true); $this->lock->release()->will(function (): void { }); - $locker->createLock(Argument::type('string'))->willReturn($this->lock->reveal()); - - $this->geolocationDbUpdater = new GeolocationDbUpdater( - $this->dbUpdater->reveal(), - $this->geoLiteDbReader->reveal(), - $locker->reveal(), - $this->trackingOptions, - ); } /** @test */ @@ -64,7 +54,7 @@ class GeolocationDbUpdaterTest extends TestCase $download = $this->dbUpdater->downloadFreshCopy(null)->willThrow($prev); try { - $this->geolocationDbUpdater->checkDbUpdate($mustBeUpdated); + $this->geolocationDbUpdater()->checkDbUpdate($mustBeUpdated); self::assertTrue(false); // If this is reached, the test will fail } catch (Throwable $e) { /** @var GeolocationDbUpdateFailedException $e */ @@ -94,7 +84,7 @@ class GeolocationDbUpdaterTest extends TestCase $download = $this->dbUpdater->downloadFreshCopy(null)->willThrow($prev); try { - $this->geolocationDbUpdater->checkDbUpdate(); + $this->geolocationDbUpdater()->checkDbUpdate(); self::assertTrue(false); // If this is reached, the test will fail } catch (Throwable $e) { /** @var GeolocationDbUpdateFailedException $e */ @@ -127,7 +117,7 @@ class GeolocationDbUpdaterTest extends TestCase $download = $this->dbUpdater->downloadFreshCopy(null)->will(function (): void { }); - $this->geolocationDbUpdater->checkDbUpdate(); + $this->geolocationDbUpdater()->checkDbUpdate(); $fileExists->shouldHaveBeenCalledOnce(); $getMeta->shouldHaveBeenCalledOnce(); @@ -160,7 +150,7 @@ class GeolocationDbUpdaterTest extends TestCase $getMeta->shouldBeCalledOnce(); $download->shouldNotBeCalled(); - $this->geolocationDbUpdater->checkDbUpdate(); + $this->geolocationDbUpdater()->checkDbUpdate(); } private function buildMetaWithBuildEpoch(string|int $buildEpoch): Metadata @@ -182,13 +172,9 @@ class GeolocationDbUpdaterTest extends TestCase * @test * @dataProvider provideTrackingOptions */ - public function downloadDbIsSkippedIfTrackingIsDisabled(array $props): void + public function downloadDbIsSkippedIfTrackingIsDisabled(TrackingOptions $options): void { - foreach ($props as $prop) { - $this->trackingOptions->{$prop} = true; - } - - $this->geolocationDbUpdater->checkDbUpdate(); + $this->geolocationDbUpdater($options)->checkDbUpdate(); $this->dbUpdater->databaseFileExists(Argument::cetera())->shouldNotHaveBeenCalled(); $this->geoLiteDbReader->metadata(Argument::cetera())->shouldNotHaveBeenCalled(); @@ -196,8 +182,21 @@ class GeolocationDbUpdaterTest extends TestCase public function provideTrackingOptions(): iterable { - yield 'disableTracking' => [['disableTracking']]; - yield 'disableIpTracking' => [['disableIpTracking']]; - yield 'both' => [['disableTracking', 'disableIpTracking']]; + yield 'disableTracking' => [new TrackingOptions(disableTracking: true)]; + yield 'disableIpTracking' => [new TrackingOptions(disableIpTracking: true)]; + yield 'both' => [new TrackingOptions(disableTracking: true, disableIpTracking: true)]; + } + + private function geolocationDbUpdater(?TrackingOptions $options = null): GeolocationDbUpdater + { + $locker = $this->prophesize(Lock\LockFactory::class); + $locker->createLock(Argument::type('string'))->willReturn($this->lock->reveal()); + + return new GeolocationDbUpdater( + $this->dbUpdater->reveal(), + $this->geoLiteDbReader->reveal(), + $locker->reveal(), + $options ?? new TrackingOptions(), + ); } } diff --git a/module/Core/config/dependencies.config.php b/module/Core/config/dependencies.config.php index 9edc5fc2..d6cbd72a 100644 --- a/module/Core/config/dependencies.config.php +++ b/module/Core/config/dependencies.config.php @@ -7,6 +7,7 @@ namespace Shlinkio\Shlink\Core; use Laminas\ServiceManager\AbstractFactory\ConfigAbstractFactory; use Laminas\ServiceManager\Factory\InvokableFactory; use Psr\EventDispatcher\EventDispatcherInterface; +use Shlinkio\Shlink\Config\Factory\ValinorConfigFactory; use Shlinkio\Shlink\Core\ErrorHandler; use Shlinkio\Shlink\Core\Options\NotFoundRedirectOptions; use Shlinkio\Shlink\Importer\ImportedLinksProcessorInterface; @@ -25,7 +26,7 @@ return [ Options\NotFoundRedirectOptions::class => ConfigAbstractFactory::class, Options\RedirectOptions::class => ConfigAbstractFactory::class, Options\UrlShortenerOptions::class => ConfigAbstractFactory::class, - Options\TrackingOptions::class => ConfigAbstractFactory::class, + Options\TrackingOptions::class => [ValinorConfigFactory::class, 'config.tracking'], Options\QrCodeOptions::class => ConfigAbstractFactory::class, Options\RabbitMqOptions::class => ConfigAbstractFactory::class, Options\WebhookOptions::class => ConfigAbstractFactory::class, @@ -90,7 +91,6 @@ return [ Options\NotFoundRedirectOptions::class => ['config.not_found_redirects'], Options\RedirectOptions::class => ['config.redirects'], Options\UrlShortenerOptions::class => ['config.url_shortener'], - Options\TrackingOptions::class => ['config.tracking'], Options\QrCodeOptions::class => ['config.qr_codes'], Options\RabbitMqOptions::class => ['config.rabbitmq'], Options\WebhookOptions::class => ['config.visits_webhooks'], diff --git a/module/Core/src/Model/Visitor.php b/module/Core/src/Model/Visitor.php index 2207fad8..61663b95 100644 --- a/module/Core/src/Model/Visitor.php +++ b/module/Core/src/Model/Visitor.php @@ -69,9 +69,9 @@ final class Visitor public function normalizeForTrackingOptions(TrackingOptions $options): self { $instance = new self( - $options->disableUaTracking() ? '' : $this->userAgent, - $options->disableReferrerTracking() ? '' : $this->referer, - $options->disableIpTracking() ? null : $this->remoteAddress, + $options->disableUaTracking ? '' : $this->userAgent, + $options->disableReferrerTracking ? '' : $this->referer, + $options->disableIpTracking ? null : $this->remoteAddress, $this->visitedUrl, ); diff --git a/module/Core/src/Options/TrackingOptions.php b/module/Core/src/Options/TrackingOptions.php index ba51b8e9..d4272374 100644 --- a/module/Core/src/Options/TrackingOptions.php +++ b/module/Core/src/Options/TrackingOptions.php @@ -4,103 +4,21 @@ declare(strict_types=1); namespace Shlinkio\Shlink\Core\Options; -use Laminas\Stdlib\AbstractOptions; - use function array_key_exists; -use function explode; -use function Functional\map; -use function is_array; -use function trim; -class TrackingOptions extends AbstractOptions +final class TrackingOptions { - private bool $anonymizeRemoteAddr = true; - private bool $trackOrphanVisits = true; - private ?string $disableTrackParam = null; - private bool $disableTracking = false; - private bool $disableIpTracking = false; - private bool $disableReferrerTracking = false; - private bool $disableUaTracking = false; - private array $disableTrackingFrom = []; - - public function anonymizeRemoteAddr(): bool - { - return $this->anonymizeRemoteAddr; - } - - protected function setAnonymizeRemoteAddr(bool $anonymizeRemoteAddr): void - { - $this->anonymizeRemoteAddr = $anonymizeRemoteAddr; - } - - public function trackOrphanVisits(): bool - { - return $this->trackOrphanVisits; - } - - protected function setTrackOrphanVisits(bool $trackOrphanVisits): void - { - $this->trackOrphanVisits = $trackOrphanVisits; - } - - public function getDisableTrackParam(): ?string - { - return $this->disableTrackParam; - } - - public function queryHasDisableTrackParam(array $query): bool - { - return $this->disableTrackParam !== null && array_key_exists($this->disableTrackParam, $query); - } - - protected function setDisableTrackParam(?string $disableTrackParam): void - { - $this->disableTrackParam = $disableTrackParam; - } - - public function disableTracking(): bool - { - return $this->disableTracking; - } - - protected function setDisableTracking(bool $disableTracking): void - { - $this->disableTracking = $disableTracking; - } - - public function disableIpTracking(): bool - { - return $this->disableIpTracking; - } - - protected function setDisableIpTracking(bool $disableIpTracking): void - { - $this->disableIpTracking = $disableIpTracking; - } - - public function disableReferrerTracking(): bool - { - return $this->disableReferrerTracking; - } - - protected function setDisableReferrerTracking(bool $disableReferrerTracking): void - { - $this->disableReferrerTracking = $disableReferrerTracking; - } - - public function disableUaTracking(): bool - { - return $this->disableUaTracking; - } - - protected function setDisableUaTracking(bool $disableUaTracking): void - { - $this->disableUaTracking = $disableUaTracking; - } - - public function disableTrackingFrom(): array - { - return $this->disableTrackingFrom; + public function __construct( + public readonly bool $anonymizeRemoteAddr = true, + public readonly bool $trackOrphanVisits = true, + public readonly ?string $disableTrackParam = null, + public readonly bool $disableTracking = false, + public readonly bool $disableIpTracking = false, + public readonly bool $disableReferrerTracking = false, + public readonly bool $disableUaTracking = false, + /** @var string[] */ + public readonly array $disableTrackingFrom = [], + ) { } public function hasDisableTrackingFrom(): bool @@ -108,12 +26,8 @@ class TrackingOptions extends AbstractOptions return ! empty($this->disableTrackingFrom); } - protected function setDisableTrackingFrom(string|array|null $disableTrackingFrom): void + public function queryHasDisableTrackParam(array $query): bool { - $this->disableTrackingFrom = match (true) { - is_array($disableTrackingFrom) => $disableTrackingFrom, - $disableTrackingFrom === null => [], - default => map(explode(',', $disableTrackingFrom), static fn (string $value) => trim($value)), - }; + return $this->disableTrackParam !== null && array_key_exists($this->disableTrackParam, $query); } } diff --git a/module/Core/src/ShortUrl/Helper/ShortUrlRedirectionBuilder.php b/module/Core/src/ShortUrl/Helper/ShortUrlRedirectionBuilder.php index 3251922d..985e2a3f 100644 --- a/module/Core/src/ShortUrl/Helper/ShortUrlRedirectionBuilder.php +++ b/module/Core/src/ShortUrl/Helper/ShortUrlRedirectionBuilder.php @@ -33,7 +33,7 @@ class ShortUrlRedirectionBuilder implements ShortUrlRedirectionBuilderInterface { $hardcodedQuery = Query::parse($uri->getQuery() ?? ''); - $disableTrackParam = $this->trackingOptions->getDisableTrackParam(); + $disableTrackParam = $this->trackingOptions->disableTrackParam; if ($disableTrackParam !== null) { unset($currentQuery[$disableTrackParam]); } diff --git a/module/Core/src/Visit/RequestTracker.php b/module/Core/src/Visit/RequestTracker.php index 1887dbfd..084dfcf8 100644 --- a/module/Core/src/Visit/RequestTracker.php +++ b/module/Core/src/Visit/RequestTracker.php @@ -83,7 +83,7 @@ class RequestTracker implements RequestTrackerInterface, RequestMethodInterface } $remoteAddrParts = explode('.', $remoteAddr); - $disableTrackingFrom = $this->trackingOptions->disableTrackingFrom(); + $disableTrackingFrom = $this->trackingOptions->disableTrackingFrom; return some($disableTrackingFrom, function (string $value) use ($ip, $remoteAddrParts): bool { $range = str_contains($value, '*') diff --git a/module/Core/src/Visit/VisitsTracker.php b/module/Core/src/Visit/VisitsTracker.php index 585a0f86..1b30155c 100644 --- a/module/Core/src/Visit/VisitsTracker.php +++ b/module/Core/src/Visit/VisitsTracker.php @@ -24,7 +24,7 @@ class VisitsTracker implements VisitsTrackerInterface public function track(ShortUrl $shortUrl, Visitor $visitor): void { $this->trackVisit( - fn (Visitor $v) => Visit::forValidShortUrl($shortUrl, $v, $this->options->anonymizeRemoteAddr()), + fn (Visitor $v) => Visit::forValidShortUrl($shortUrl, $v, $this->options->anonymizeRemoteAddr), $visitor, ); } @@ -32,7 +32,7 @@ class VisitsTracker implements VisitsTrackerInterface public function trackInvalidShortUrlVisit(Visitor $visitor): void { $this->trackOrphanVisit( - fn (Visitor $v) => Visit::forInvalidShortUrl($v, $this->options->anonymizeRemoteAddr()), + fn (Visitor $v) => Visit::forInvalidShortUrl($v, $this->options->anonymizeRemoteAddr), $visitor, ); } @@ -40,7 +40,7 @@ class VisitsTracker implements VisitsTrackerInterface public function trackBaseUrlVisit(Visitor $visitor): void { $this->trackOrphanVisit( - fn (Visitor $v) => Visit::forBasePath($v, $this->options->anonymizeRemoteAddr()), + fn (Visitor $v) => Visit::forBasePath($v, $this->options->anonymizeRemoteAddr), $visitor, ); } @@ -48,14 +48,14 @@ class VisitsTracker implements VisitsTrackerInterface public function trackRegularNotFoundVisit(Visitor $visitor): void { $this->trackOrphanVisit( - fn (Visitor $v) => Visit::forRegularNotFound($v, $this->options->anonymizeRemoteAddr()), + fn (Visitor $v) => Visit::forRegularNotFound($v, $this->options->anonymizeRemoteAddr), $visitor, ); } private function trackOrphanVisit(callable $createVisit, Visitor $visitor): void { - if (! $this->options->trackOrphanVisits()) { + if (! $this->options->trackOrphanVisits) { return; } @@ -64,7 +64,7 @@ class VisitsTracker implements VisitsTrackerInterface private function trackVisit(callable $createVisit, Visitor $visitor): void { - if ($this->options->disableTracking()) { + if ($this->options->disableTracking) { return; } diff --git a/module/Core/test/Model/VisitorTest.php b/module/Core/test/Model/VisitorTest.php index 92a46a16..92c21157 100644 --- a/module/Core/test/Model/VisitorTest.php +++ b/module/Core/test/Model/VisitorTest.php @@ -82,11 +82,11 @@ class VisitorTest extends TestCase $this->generateRandomString(2000), $this->generateRandomString(2000), ); - $normalizedVisitor = $visitor->normalizeForTrackingOptions(new TrackingOptions([ - 'disableIpTracking' => true, - 'disableReferrerTracking' => true, - 'disableUaTracking' => true, - ])); + $normalizedVisitor = $visitor->normalizeForTrackingOptions(new TrackingOptions( + disableIpTracking: true, + disableReferrerTracking: true, + disableUaTracking: true, + )); self::assertNotSame($visitor, $normalizedVisitor); self::assertEmpty($normalizedVisitor->userAgent); diff --git a/module/Core/test/ShortUrl/Helper/ShortUrlRedirectionBuilderTest.php b/module/Core/test/ShortUrl/Helper/ShortUrlRedirectionBuilderTest.php index 829d77ea..97b35f2b 100644 --- a/module/Core/test/ShortUrl/Helper/ShortUrlRedirectionBuilderTest.php +++ b/module/Core/test/ShortUrl/Helper/ShortUrlRedirectionBuilderTest.php @@ -16,7 +16,7 @@ class ShortUrlRedirectionBuilderTest extends TestCase protected function setUp(): void { - $trackingOptions = new TrackingOptions(['disable_track_param' => 'foobar']); + $trackingOptions = new TrackingOptions(disableTrackParam: 'foobar'); $this->redirectionBuilder = new ShortUrlRedirectionBuilder($trackingOptions); } diff --git a/module/Core/test/Visit/RequestTrackerTest.php b/module/Core/test/Visit/RequestTrackerTest.php index 144087ad..4634004f 100644 --- a/module/Core/test/Visit/RequestTrackerTest.php +++ b/module/Core/test/Visit/RequestTrackerTest.php @@ -38,10 +38,10 @@ class RequestTrackerTest extends TestCase $this->requestTracker = new RequestTracker( $this->visitsTracker->reveal(), - new TrackingOptions([ - 'disable_track_param' => 'foobar', - 'disable_tracking_from' => ['80.90.100.110', '192.168.10.0/24', '1.2.*.*'], - ]), + new TrackingOptions( + disableTrackParam: 'foobar', + disableTrackingFrom: ['80.90.100.110', '192.168.10.0/24', '1.2.*.*'], + ), ); $this->request = ServerRequestFactory::fromGlobals()->withAttribute( diff --git a/module/Core/test/Visit/VisitsTrackerTest.php b/module/Core/test/Visit/VisitsTrackerTest.php index 2bb13220..72028543 100644 --- a/module/Core/test/Visit/VisitsTrackerTest.php +++ b/module/Core/test/Visit/VisitsTrackerTest.php @@ -24,16 +24,11 @@ class VisitsTrackerTest extends TestCase private VisitsTracker $visitsTracker; private ObjectProphecy $em; private ObjectProphecy $eventDispatcher; - private TrackingOptions $options; protected function setUp(): void { $this->em = $this->prophesize(EntityManager::class); - $this->eventDispatcher = $this->prophesize(EventDispatcherInterface::class); - $this->options = new TrackingOptions(); - - $this->visitsTracker = new VisitsTracker($this->em->reveal(), $this->eventDispatcher->reveal(), $this->options); } /** @@ -45,7 +40,7 @@ class VisitsTrackerTest extends TestCase $persist = $this->em->persist(Argument::that(fn (Visit $visit) => $visit->setId('1')))->will(function (): void { }); - $this->visitsTracker->{$method}(...$args); + $this->visitsTracker()->{$method}(...$args); $persist->shouldHaveBeenCalledOnce(); $this->em->flush()->shouldHaveBeenCalledOnce(); @@ -58,9 +53,7 @@ class VisitsTrackerTest extends TestCase */ public function trackingIsSkippedCompletelyWhenDisabledFromOptions(string $method, array $args): void { - $this->options->disableTracking = true; - - $this->visitsTracker->{$method}(...$args); + $this->visitsTracker(new TrackingOptions(disableTracking: true))->{$method}(...$args); $this->eventDispatcher->dispatch(Argument::cetera())->shouldNotHaveBeenCalled(); $this->em->persist(Argument::cetera())->shouldNotHaveBeenCalled(); @@ -81,9 +74,7 @@ class VisitsTrackerTest extends TestCase */ public function orphanVisitsAreNotTrackedWhenDisabled(string $method): void { - $this->options->trackOrphanVisits = false; - - $this->visitsTracker->{$method}(Visitor::emptyInstance()); + $this->visitsTracker(new TrackingOptions(trackOrphanVisits: false))->{$method}(Visitor::emptyInstance()); $this->eventDispatcher->dispatch(Argument::cetera())->shouldNotHaveBeenCalled(); $this->em->persist(Argument::cetera())->shouldNotHaveBeenCalled(); @@ -96,4 +87,13 @@ class VisitsTrackerTest extends TestCase yield 'trackBaseUrlVisit' => ['trackBaseUrlVisit']; yield 'trackRegularNotFoundVisit' => ['trackRegularNotFoundVisit']; } + + private function visitsTracker(?TrackingOptions $options = null): VisitsTracker + { + return new VisitsTracker( + $this->em->reveal(), + $this->eventDispatcher->reveal(), + $options ?? new TrackingOptions(), + ); + } } From 96859298247fe3008df70ee58155893bcf07bce3 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 17 Sep 2022 13:01:28 +0200 Subject: [PATCH 089/108] Migrated AppOptions to immutable object --- module/CLI/src/Factory/ApplicationFactory.php | 2 +- module/Core/config/dependencies.config.php | 3 +-- module/Core/src/Options/AppOptions.php | 27 ++----------------- .../NotifyVisitToWebHooksTest.php | 2 +- module/Rest/src/Action/HealthAction.php | 2 +- module/Rest/test/Action/HealthActionTest.php | 2 +- 6 files changed, 7 insertions(+), 31 deletions(-) diff --git a/module/CLI/src/Factory/ApplicationFactory.php b/module/CLI/src/Factory/ApplicationFactory.php index 262238a3..ab716f7e 100644 --- a/module/CLI/src/Factory/ApplicationFactory.php +++ b/module/CLI/src/Factory/ApplicationFactory.php @@ -17,7 +17,7 @@ class ApplicationFactory $appOptions = $container->get(AppOptions::class); $commands = $config['commands'] ?? []; - $app = new CliApp($appOptions->getName(), $appOptions->getVersion()); + $app = new CliApp($appOptions->name, $appOptions->version); $app->setCommandLoader(new ContainerCommandLoader($container, $commands)); return $app; diff --git a/module/Core/config/dependencies.config.php b/module/Core/config/dependencies.config.php index d6cbd72a..54a861e6 100644 --- a/module/Core/config/dependencies.config.php +++ b/module/Core/config/dependencies.config.php @@ -21,7 +21,7 @@ return [ ErrorHandler\NotFoundRedirectHandler::class => ConfigAbstractFactory::class, ErrorHandler\NotFoundTemplateHandler::class => InvokableFactory::class, - Options\AppOptions::class => ConfigAbstractFactory::class, + Options\AppOptions::class => [ValinorConfigFactory::class, 'config.app_options'], Options\DeleteShortUrlsOptions::class => ConfigAbstractFactory::class, Options\NotFoundRedirectOptions::class => ConfigAbstractFactory::class, Options\RedirectOptions::class => ConfigAbstractFactory::class, @@ -86,7 +86,6 @@ return [ Domain\DomainService::class, ], - Options\AppOptions::class => ['config.app_options'], Options\DeleteShortUrlsOptions::class => ['config.delete_short_urls'], Options\NotFoundRedirectOptions::class => ['config.not_found_redirects'], Options\RedirectOptions::class => ['config.redirects'], diff --git a/module/Core/src/Options/AppOptions.php b/module/Core/src/Options/AppOptions.php index e81f9fdb..ec545352 100644 --- a/module/Core/src/Options/AppOptions.php +++ b/module/Core/src/Options/AppOptions.php @@ -4,35 +4,12 @@ declare(strict_types=1); namespace Shlinkio\Shlink\Core\Options; -use Laminas\Stdlib\AbstractOptions; - use function sprintf; -class AppOptions extends AbstractOptions +final class AppOptions { - private string $name = 'Shlink'; - private string $version = '3.0.0'; - - public function getName(): string + public function __construct(public string $name = 'Shlink', public string $version = '3.0.0') { - return $this->name; - } - - protected function setName(string $name): self - { - $this->name = $name; - return $this; - } - - public function getVersion(): string - { - return $this->version; - } - - protected function setVersion(string $version): self - { - $this->version = $version; - return $this; } public function __toString(): string diff --git a/module/Core/test/EventDispatcher/NotifyVisitToWebHooksTest.php b/module/Core/test/EventDispatcher/NotifyVisitToWebHooksTest.php index 6be8719a..4234a188 100644 --- a/module/Core/test/EventDispatcher/NotifyVisitToWebHooksTest.php +++ b/module/Core/test/EventDispatcher/NotifyVisitToWebHooksTest.php @@ -165,7 +165,7 @@ class NotifyVisitToWebHooksTest extends TestCase ['webhooks' => $webhooks, 'notify_orphan_visits_to_webhooks' => $notifyOrphanVisits], ), new ShortUrlDataTransformer(new ShortUrlStringifier([])), - new AppOptions(['name' => 'Shlink', 'version' => '1.2.3']), + new AppOptions('Shlink', '1.2.3'), ); } } diff --git a/module/Rest/src/Action/HealthAction.php b/module/Rest/src/Action/HealthAction.php index 462eb345..f3bfea98 100644 --- a/module/Rest/src/Action/HealthAction.php +++ b/module/Rest/src/Action/HealthAction.php @@ -42,7 +42,7 @@ class HealthAction extends AbstractRestAction $statusCode = $connected ? self::STATUS_OK : self::STATUS_SERVICE_UNAVAILABLE; return new JsonResponse([ 'status' => $connected ? self::STATUS_PASS : self::STATUS_FAIL, - 'version' => $this->options->getVersion(), + 'version' => $this->options->version, 'links' => [ 'about' => 'https://shlink.io', 'project' => 'https://github.com/shlinkio/shlink', diff --git a/module/Rest/test/Action/HealthActionTest.php b/module/Rest/test/Action/HealthActionTest.php index 8298b2d1..461152a4 100644 --- a/module/Rest/test/Action/HealthActionTest.php +++ b/module/Rest/test/Action/HealthActionTest.php @@ -36,7 +36,7 @@ class HealthActionTest extends TestCase $em = $this->prophesize(EntityManagerInterface::class); $em->getConnection()->willReturn($this->conn->reveal()); - $this->action = new HealthAction($em->reveal(), new AppOptions(['version' => '1.2.3'])); + $this->action = new HealthAction($em->reveal(), new AppOptions(version: '1.2.3')); } /** @test */ From 784908420e5318ca76728611a469d05b06903cf2 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 17 Sep 2022 13:04:49 +0200 Subject: [PATCH 090/108] Migrated DeleteShortUrlsOptions to immutable object --- module/Core/config/dependencies.config.php | 3 +- .../src/Options/DeleteShortUrlsOptions.php | 31 +++---------------- .../ShortUrl/DeleteShortUrlService.php | 6 ++-- .../ShortUrl/DeleteShortUrlServiceTest.php | 8 ++--- 4 files changed, 13 insertions(+), 35 deletions(-) diff --git a/module/Core/config/dependencies.config.php b/module/Core/config/dependencies.config.php index 54a861e6..1ce6414f 100644 --- a/module/Core/config/dependencies.config.php +++ b/module/Core/config/dependencies.config.php @@ -22,7 +22,7 @@ return [ ErrorHandler\NotFoundTemplateHandler::class => InvokableFactory::class, Options\AppOptions::class => [ValinorConfigFactory::class, 'config.app_options'], - Options\DeleteShortUrlsOptions::class => ConfigAbstractFactory::class, + Options\DeleteShortUrlsOptions::class => [ValinorConfigFactory::class, 'config.delete_short_urls'], Options\NotFoundRedirectOptions::class => ConfigAbstractFactory::class, Options\RedirectOptions::class => ConfigAbstractFactory::class, Options\UrlShortenerOptions::class => ConfigAbstractFactory::class, @@ -86,7 +86,6 @@ return [ Domain\DomainService::class, ], - Options\DeleteShortUrlsOptions::class => ['config.delete_short_urls'], Options\NotFoundRedirectOptions::class => ['config.not_found_redirects'], Options\RedirectOptions::class => ['config.redirects'], Options\UrlShortenerOptions::class => ['config.url_shortener'], diff --git a/module/Core/src/Options/DeleteShortUrlsOptions.php b/module/Core/src/Options/DeleteShortUrlsOptions.php index ff1c356a..a645181b 100644 --- a/module/Core/src/Options/DeleteShortUrlsOptions.php +++ b/module/Core/src/Options/DeleteShortUrlsOptions.php @@ -4,34 +4,13 @@ declare(strict_types=1); namespace Shlinkio\Shlink\Core\Options; -use Laminas\Stdlib\AbstractOptions; - use const Shlinkio\Shlink\DEFAULT_DELETE_SHORT_URL_THRESHOLD; -class DeleteShortUrlsOptions extends AbstractOptions +final class DeleteShortUrlsOptions { - private int $visitsThreshold = DEFAULT_DELETE_SHORT_URL_THRESHOLD; - private bool $checkVisitsThreshold = true; - - public function getVisitsThreshold(): int - { - return $this->visitsThreshold; - } - - protected function setVisitsThreshold(int $visitsThreshold): self - { - $this->visitsThreshold = $visitsThreshold; - return $this; - } - - public function doCheckVisitsThreshold(): bool - { - return $this->checkVisitsThreshold; - } - - protected function setCheckVisitsThreshold(bool $checkVisitsThreshold): self - { - $this->checkVisitsThreshold = $checkVisitsThreshold; - return $this; + public function __construct( + public readonly int $visitsThreshold = DEFAULT_DELETE_SHORT_URL_THRESHOLD, + public readonly bool $checkVisitsThreshold = true, + ) { } } diff --git a/module/Core/src/Service/ShortUrl/DeleteShortUrlService.php b/module/Core/src/Service/ShortUrl/DeleteShortUrlService.php index e6f2e82d..d4d6803f 100644 --- a/module/Core/src/Service/ShortUrl/DeleteShortUrlService.php +++ b/module/Core/src/Service/ShortUrl/DeleteShortUrlService.php @@ -32,7 +32,7 @@ class DeleteShortUrlService implements DeleteShortUrlServiceInterface $shortUrl = $this->urlResolver->resolveShortUrl($identifier, $apiKey); if (! $ignoreThreshold && $this->isThresholdReached($shortUrl)) { throw Exception\DeleteShortUrlException::fromVisitsThreshold( - $this->deleteShortUrlsOptions->getVisitsThreshold(), + $this->deleteShortUrlsOptions->visitsThreshold, $identifier, ); } @@ -43,10 +43,10 @@ class DeleteShortUrlService implements DeleteShortUrlServiceInterface private function isThresholdReached(ShortUrl $shortUrl): bool { - if (! $this->deleteShortUrlsOptions->doCheckVisitsThreshold()) { + if (! $this->deleteShortUrlsOptions->checkVisitsThreshold) { return false; } - return $shortUrl->getVisitsCount() >= $this->deleteShortUrlsOptions->getVisitsThreshold(); + return $shortUrl->getVisitsCount() >= $this->deleteShortUrlsOptions->visitsThreshold; } } diff --git a/module/Core/test/Service/ShortUrl/DeleteShortUrlServiceTest.php b/module/Core/test/Service/ShortUrl/DeleteShortUrlServiceTest.php index 391d52fd..87a6582f 100644 --- a/module/Core/test/Service/ShortUrl/DeleteShortUrlServiceTest.php +++ b/module/Core/test/Service/ShortUrl/DeleteShortUrlServiceTest.php @@ -102,9 +102,9 @@ class DeleteShortUrlServiceTest extends TestCase private function createService(bool $checkVisitsThreshold = true, int $visitsThreshold = 5): DeleteShortUrlService { - return new DeleteShortUrlService($this->em->reveal(), new DeleteShortUrlsOptions([ - 'visitsThreshold' => $visitsThreshold, - 'checkVisitsThreshold' => $checkVisitsThreshold, - ]), $this->urlResolver->reveal()); + return new DeleteShortUrlService($this->em->reveal(), new DeleteShortUrlsOptions( + $visitsThreshold, + $checkVisitsThreshold, + ), $this->urlResolver->reveal()); } } From 39693ca1fe178cdbefe4f7bffad6cd2e69369047 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 17 Sep 2022 13:19:17 +0200 Subject: [PATCH 091/108] Added --thread=max to infection command --- composer.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 8a53f16c..a2a30bc8 100644 --- a/composer.json +++ b/composer.json @@ -61,7 +61,7 @@ "cebe/php-openapi": "^1.7", "devster/ubench": "^2.1", "dms/phpunit-arraysubset-asserts": "^0.4.0", - "infection/infection": "^0.26.5", + "infection/infection": "^0.26.15", "openswoole/ide-helper": "~4.11.1", "phpspec/prophecy-phpunit": "^2.0", "phpstan/phpstan": "^1.8", @@ -129,7 +129,7 @@ "test:cli": "APP_ENV=test DB_DRIVER=maria TEST_ENV=cli php vendor/bin/phpunit --order-by=random --colors=always --testdox -c phpunit-cli.xml --log-junit=build/coverage-cli/junit.xml", "test:cli:ci": "GENERATE_COVERAGE=yes composer test:cli", "test:cli:pretty": "GENERATE_COVERAGE=pretty composer test:cli", - "infect:ci:base": "infection --threads=4 --only-covered --only-covering-test-cases --skip-initial-tests", + "infect:ci:base": "infection --threads=max --only-covered --only-covering-test-cases --skip-initial-tests", "infect:ci:unit": "@infect:ci:base --coverage=build/coverage-unit --min-msi=84", "infect:ci:db": "@infect:ci:base --coverage=build/coverage-db --min-msi=95 --configuration=infection-db.json", "infect:ci:api": "@infect:ci:base --coverage=build/coverage-api --min-msi=80 --configuration=infection-api.json", From 20f457a3e96d148dbfc8d6cc6bafb5dfd1c67e62 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 17 Sep 2022 13:32:40 +0200 Subject: [PATCH 092/108] Migrated NotFoundRedirectOptions to immutable object --- .../Command/Domain/ListDomainsCommandTest.php | 8 ++--- module/Core/config/dependencies.config.php | 3 +- .../src/Options/NotFoundRedirectOptions.php | 30 +++++-------------- .../Config/NotFoundRedirectResolverTest.php | 22 +++++++------- .../Request/DomainRedirectsRequestTest.php | 4 +-- 5 files changed, 25 insertions(+), 42 deletions(-) diff --git a/module/CLI/test/Command/Domain/ListDomainsCommandTest.php b/module/CLI/test/Command/Domain/ListDomainsCommandTest.php index adaa1d00..51da498b 100644 --- a/module/CLI/test/Command/Domain/ListDomainsCommandTest.php +++ b/module/CLI/test/Command/Domain/ListDomainsCommandTest.php @@ -43,10 +43,10 @@ class ListDomainsCommandTest extends TestCase )); $listDomains = $this->domainService->listDomains()->willReturn([ - DomainItem::forDefaultDomain('foo.com', new NotFoundRedirectOptions([ - 'base_url' => 'https://foo.com/default/base', - 'invalid_short_url' => 'https://foo.com/default/invalid', - ])), + DomainItem::forDefaultDomain('foo.com', new NotFoundRedirectOptions( + invalidShortUrl: 'https://foo.com/default/invalid', + baseUrl: 'https://foo.com/default/base', + )), DomainItem::forNonDefaultDomain(Domain::withAuthority('bar.com')), DomainItem::forNonDefaultDomain($bazDomain), ]); diff --git a/module/Core/config/dependencies.config.php b/module/Core/config/dependencies.config.php index 1ce6414f..80926dc1 100644 --- a/module/Core/config/dependencies.config.php +++ b/module/Core/config/dependencies.config.php @@ -23,7 +23,7 @@ return [ Options\AppOptions::class => [ValinorConfigFactory::class, 'config.app_options'], Options\DeleteShortUrlsOptions::class => [ValinorConfigFactory::class, 'config.delete_short_urls'], - Options\NotFoundRedirectOptions::class => ConfigAbstractFactory::class, + Options\NotFoundRedirectOptions::class => [ValinorConfigFactory::class, 'config.not_found_redirects'], Options\RedirectOptions::class => ConfigAbstractFactory::class, Options\UrlShortenerOptions::class => ConfigAbstractFactory::class, Options\TrackingOptions::class => [ValinorConfigFactory::class, 'config.tracking'], @@ -86,7 +86,6 @@ return [ Domain\DomainService::class, ], - Options\NotFoundRedirectOptions::class => ['config.not_found_redirects'], Options\RedirectOptions::class => ['config.redirects'], Options\UrlShortenerOptions::class => ['config.url_shortener'], Options\QrCodeOptions::class => ['config.qr_codes'], diff --git a/module/Core/src/Options/NotFoundRedirectOptions.php b/module/Core/src/Options/NotFoundRedirectOptions.php index 2f2d813b..fe99ac7e 100644 --- a/module/Core/src/Options/NotFoundRedirectOptions.php +++ b/module/Core/src/Options/NotFoundRedirectOptions.php @@ -4,14 +4,16 @@ declare(strict_types=1); namespace Shlinkio\Shlink\Core\Options; -use Laminas\Stdlib\AbstractOptions; use Shlinkio\Shlink\Core\Config\NotFoundRedirectConfigInterface; -class NotFoundRedirectOptions extends AbstractOptions implements NotFoundRedirectConfigInterface +final class NotFoundRedirectOptions implements NotFoundRedirectConfigInterface { - private ?string $invalidShortUrl = null; - private ?string $regular404 = null; - private ?string $baseUrl = null; + public function __construct( + public readonly ?string $invalidShortUrl = null, + public readonly ?string $regular404 = null, + public readonly ?string $baseUrl = null, + ) { + } public function invalidShortUrlRedirect(): ?string { @@ -23,12 +25,6 @@ class NotFoundRedirectOptions extends AbstractOptions implements NotFoundRedirec return $this->invalidShortUrl !== null; } - protected function setInvalidShortUrl(?string $invalidShortUrl): self - { - $this->invalidShortUrl = $invalidShortUrl; - return $this; - } - public function regular404Redirect(): ?string { return $this->regular404; @@ -39,12 +35,6 @@ class NotFoundRedirectOptions extends AbstractOptions implements NotFoundRedirec return $this->regular404 !== null; } - protected function setRegular404(?string $regular404): self - { - $this->regular404 = $regular404; - return $this; - } - public function baseUrlRedirect(): ?string { return $this->baseUrl; @@ -54,10 +44,4 @@ class NotFoundRedirectOptions extends AbstractOptions implements NotFoundRedirec { return $this->baseUrl !== null; } - - protected function setBaseUrl(?string $baseUrl): self - { - $this->baseUrl = $baseUrl; - return $this; - } } diff --git a/module/Core/test/Config/NotFoundRedirectResolverTest.php b/module/Core/test/Config/NotFoundRedirectResolverTest.php index aa98d102..912e17a5 100644 --- a/module/Core/test/Config/NotFoundRedirectResolverTest.php +++ b/module/Core/test/Config/NotFoundRedirectResolverTest.php @@ -60,57 +60,57 @@ class NotFoundRedirectResolverTest extends TestCase yield 'base URL with trailing slash' => [ $uri = new Uri('/'), $this->notFoundType(ServerRequestFactory::fromGlobals()->withUri($uri)), - new NotFoundRedirectOptions(['baseUrl' => 'baseUrl']), + new NotFoundRedirectOptions(baseUrl: 'baseUrl'), 'baseUrl', ]; yield 'base URL with domain placeholder' => [ $uri = new Uri('https://doma.in'), $this->notFoundType(ServerRequestFactory::fromGlobals()->withUri($uri)), - new NotFoundRedirectOptions(['baseUrl' => 'https://redirect-here.com/{DOMAIN}']), + new NotFoundRedirectOptions(baseUrl: 'https://redirect-here.com/{DOMAIN}'), 'https://redirect-here.com/doma.in', ]; yield 'base URL with domain placeholder in query' => [ $uri = new Uri('https://doma.in'), $this->notFoundType(ServerRequestFactory::fromGlobals()->withUri($uri)), - new NotFoundRedirectOptions(['baseUrl' => 'https://redirect-here.com/?domain={DOMAIN}']), + new NotFoundRedirectOptions(baseUrl: 'https://redirect-here.com/?domain={DOMAIN}'), 'https://redirect-here.com/?domain=doma.in', ]; yield 'base URL without trailing slash' => [ $uri = new Uri(''), $this->notFoundType(ServerRequestFactory::fromGlobals()->withUri($uri)), - new NotFoundRedirectOptions(['baseUrl' => 'baseUrl']), + new NotFoundRedirectOptions(baseUrl: 'baseUrl'), 'baseUrl', ]; yield 'regular 404' => [ $uri = new Uri('/foo/bar'), $this->notFoundType(ServerRequestFactory::fromGlobals()->withUri($uri)), - new NotFoundRedirectOptions(['regular404' => 'regular404']), + new NotFoundRedirectOptions(regular404: 'regular404'), 'regular404', ]; yield 'regular 404 with path placeholder in query' => [ $uri = new Uri('/foo/bar'), $this->notFoundType(ServerRequestFactory::fromGlobals()->withUri($uri)), - new NotFoundRedirectOptions(['regular404' => 'https://redirect-here.com/?path={ORIGINAL_PATH}']), + new NotFoundRedirectOptions(regular404: 'https://redirect-here.com/?path={ORIGINAL_PATH}'), 'https://redirect-here.com/?path=%2Ffoo%2Fbar', ]; yield 'regular 404 with multiple placeholders' => [ $uri = new Uri('https://doma.in/foo/bar'), $this->notFoundType(ServerRequestFactory::fromGlobals()->withUri($uri)), - new NotFoundRedirectOptions([ - 'regular404' => 'https://redirect-here.com/{ORIGINAL_PATH}/{DOMAIN}/?d={DOMAIN}&p={ORIGINAL_PATH}', - ]), + new NotFoundRedirectOptions( + regular404: 'https://redirect-here.com/{ORIGINAL_PATH}/{DOMAIN}/?d={DOMAIN}&p={ORIGINAL_PATH}', + ), 'https://redirect-here.com/foo/bar/doma.in/?d=doma.in&p=%2Ffoo%2Fbar', ]; yield 'invalid short URL' => [ new Uri('/foo'), $this->notFoundType($this->requestForRoute(RedirectAction::class)), - new NotFoundRedirectOptions(['invalidShortUrl' => 'invalidShortUrl']), + new NotFoundRedirectOptions(invalidShortUrl: 'invalidShortUrl'), 'invalidShortUrl', ]; yield 'invalid short URL with path placeholder' => [ new Uri('/foo'), $this->notFoundType($this->requestForRoute(RedirectAction::class)), - new NotFoundRedirectOptions(['invalidShortUrl' => 'https://redirect-here.com/{ORIGINAL_PATH}']), + new NotFoundRedirectOptions(invalidShortUrl: 'https://redirect-here.com/{ORIGINAL_PATH}'), 'https://redirect-here.com/foo', ]; } diff --git a/module/Rest/test/Action/Domain/Request/DomainRedirectsRequestTest.php b/module/Rest/test/Action/Domain/Request/DomainRedirectsRequestTest.php index 05212fe7..51509047 100644 --- a/module/Rest/test/Action/Domain/Request/DomainRedirectsRequestTest.php +++ b/module/Rest/test/Action/Domain/Request/DomainRedirectsRequestTest.php @@ -55,7 +55,7 @@ class DomainRedirectsRequestTest extends TestCase yield 'some values' => [['domain' => 'foo', 'regular404Redirect' => 'bar'], null, 'foo', null, 'bar', null]; yield 'fallbacks' => [ ['domain' => 'domain', 'baseUrlRedirect' => 'bar'], - new NotFoundRedirectOptions(['regular404' => 'fallback', 'invalidShortUrl' => 'fallback2']), + new NotFoundRedirectOptions(invalidShortUrl: 'fallback2', regular404: 'fallback'), 'domain', 'bar', 'fallback', @@ -63,7 +63,7 @@ class DomainRedirectsRequestTest extends TestCase ]; yield 'fallback ignored' => [ ['domain' => 'domain', 'regular404Redirect' => 'bar', 'invalidShortUrlRedirect' => null], - new NotFoundRedirectOptions(['regular404' => 'fallback', 'invalidShortUrl' => 'fallback2']), + new NotFoundRedirectOptions(invalidShortUrl: 'fallback2', regular404: 'fallback'), 'domain', null, 'bar', From 0c34032fd386d8d2f0705219996a92e5e7b1ceb1 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 17 Sep 2022 13:45:09 +0200 Subject: [PATCH 093/108] Migrated QrCodeOptions to immutable object --- composer.json | 2 + module/Core/config/dependencies.config.php | 3 +- module/Core/src/Action/Model/QrCodeParams.php | 10 +- module/Core/src/Options/QrCodeOptions.php | 65 ++--------- module/Core/test/Action/QrCodeActionTest.php | 105 ++++++++++-------- 5 files changed, 77 insertions(+), 108 deletions(-) diff --git a/composer.json b/composer.json index a2a30bc8..d6122753 100644 --- a/composer.json +++ b/composer.json @@ -13,6 +13,8 @@ ], "require": { "php": "^8.1", + "ext-curl": "*", + "ext-gd": "*", "ext-json": "*", "ext-pdo": "*", "akrabat/ip-address-middleware": "^2.1", diff --git a/module/Core/config/dependencies.config.php b/module/Core/config/dependencies.config.php index 80926dc1..df63eeff 100644 --- a/module/Core/config/dependencies.config.php +++ b/module/Core/config/dependencies.config.php @@ -27,7 +27,7 @@ return [ Options\RedirectOptions::class => ConfigAbstractFactory::class, Options\UrlShortenerOptions::class => ConfigAbstractFactory::class, Options\TrackingOptions::class => [ValinorConfigFactory::class, 'config.tracking'], - Options\QrCodeOptions::class => ConfigAbstractFactory::class, + Options\QrCodeOptions::class => [ValinorConfigFactory::class, 'config.qr_codes'], Options\RabbitMqOptions::class => ConfigAbstractFactory::class, Options\WebhookOptions::class => ConfigAbstractFactory::class, @@ -88,7 +88,6 @@ return [ Options\RedirectOptions::class => ['config.redirects'], Options\UrlShortenerOptions::class => ['config.url_shortener'], - Options\QrCodeOptions::class => ['config.qr_codes'], Options\RabbitMqOptions::class => ['config.rabbitmq'], Options\WebhookOptions::class => ['config.visits_webhooks'], diff --git a/module/Core/src/Action/Model/QrCodeParams.php b/module/Core/src/Action/Model/QrCodeParams.php index 7c1f0e34..306c2b44 100644 --- a/module/Core/src/Action/Model/QrCodeParams.php +++ b/module/Core/src/Action/Model/QrCodeParams.php @@ -52,7 +52,7 @@ final class QrCodeParams private static function resolveSize(array $query, QrCodeOptions $defaults): int { - $size = (int) ($query['size'] ?? $defaults->size()); + $size = (int) ($query['size'] ?? $defaults->size); if ($size < self::MIN_SIZE) { return self::MIN_SIZE; } @@ -62,7 +62,7 @@ final class QrCodeParams private static function resolveMargin(array $query, QrCodeOptions $defaults): int { - $margin = $query['margin'] ?? (string) $defaults->margin(); + $margin = $query['margin'] ?? (string) $defaults->margin; $intMargin = (int) $margin; if ($margin !== (string) $intMargin) { return 0; @@ -74,7 +74,7 @@ final class QrCodeParams private static function resolveWriter(array $query, QrCodeOptions $defaults): WriterInterface { $qFormat = self::normalizeParam($query['format'] ?? ''); - $format = contains(self::SUPPORTED_FORMATS, $qFormat) ? $qFormat : self::normalizeParam($defaults->format()); + $format = contains(self::SUPPORTED_FORMATS, $qFormat) ? $qFormat : self::normalizeParam($defaults->format); return match ($format) { 'svg' => new SvgWriter(), @@ -84,7 +84,7 @@ final class QrCodeParams private static function resolveErrorCorrection(array $query, QrCodeOptions $defaults): ErrorCorrectionLevelInterface { - $errorCorrectionLevel = self::normalizeParam($query['errorCorrection'] ?? $defaults->errorCorrection()); + $errorCorrectionLevel = self::normalizeParam($query['errorCorrection'] ?? $defaults->errorCorrection); return match ($errorCorrectionLevel) { 'h' => new ErrorCorrectionLevelHigh(), 'q' => new ErrorCorrectionLevelQuartile(), @@ -97,7 +97,7 @@ final class QrCodeParams { $doNotRoundBlockSize = isset($query['roundBlockSize']) ? $query['roundBlockSize'] === 'false' - : ! $defaults->roundBlockSize(); + : ! $defaults->roundBlockSize; return $doNotRoundBlockSize ? new RoundBlockSizeModeNone() : new RoundBlockSizeModeMargin(); } diff --git a/module/Core/src/Options/QrCodeOptions.php b/module/Core/src/Options/QrCodeOptions.php index 3dfc9a53..1b10c280 100644 --- a/module/Core/src/Options/QrCodeOptions.php +++ b/module/Core/src/Options/QrCodeOptions.php @@ -4,69 +4,20 @@ declare(strict_types=1); namespace Shlinkio\Shlink\Core\Options; -use Laminas\Stdlib\AbstractOptions; - use const Shlinkio\Shlink\DEFAULT_QR_CODE_ERROR_CORRECTION; use const Shlinkio\Shlink\DEFAULT_QR_CODE_FORMAT; use const Shlinkio\Shlink\DEFAULT_QR_CODE_MARGIN; use const Shlinkio\Shlink\DEFAULT_QR_CODE_ROUND_BLOCK_SIZE; use const Shlinkio\Shlink\DEFAULT_QR_CODE_SIZE; -class QrCodeOptions extends AbstractOptions +final class QrCodeOptions { - private int $size = DEFAULT_QR_CODE_SIZE; - private int $margin = DEFAULT_QR_CODE_MARGIN; - private string $format = DEFAULT_QR_CODE_FORMAT; - private string $errorCorrection = DEFAULT_QR_CODE_ERROR_CORRECTION; - private bool $roundBlockSize = DEFAULT_QR_CODE_ROUND_BLOCK_SIZE; - - public function size(): int - { - return $this->size; - } - - protected function setSize(int $size): void - { - $this->size = $size; - } - - public function margin(): int - { - return $this->margin; - } - - protected function setMargin(int $margin): void - { - $this->margin = $margin; - } - - public function format(): string - { - return $this->format; - } - - protected function setFormat(string $format): void - { - $this->format = $format; - } - - public function errorCorrection(): string - { - return $this->errorCorrection; - } - - protected function setErrorCorrection(string $errorCorrection): void - { - $this->errorCorrection = $errorCorrection; - } - - public function roundBlockSize(): bool - { - return $this->roundBlockSize; - } - - protected function setRoundBlockSize(bool $roundBlockSize): void - { - $this->roundBlockSize = $roundBlockSize; + public function __construct( + public readonly int $size = DEFAULT_QR_CODE_SIZE, + public readonly int $margin = DEFAULT_QR_CODE_MARGIN, + public readonly string $format = DEFAULT_QR_CODE_FORMAT, + public readonly string $errorCorrection = DEFAULT_QR_CODE_ERROR_CORRECTION, + public readonly bool $roundBlockSize = DEFAULT_QR_CODE_ROUND_BLOCK_SIZE, + ) { } } diff --git a/module/Core/test/Action/QrCodeActionTest.php b/module/Core/test/Action/QrCodeActionTest.php index 1962fdc7..1f71975f 100644 --- a/module/Core/test/Action/QrCodeActionTest.php +++ b/module/Core/test/Action/QrCodeActionTest.php @@ -7,7 +7,6 @@ namespace ShlinkioTest\Shlink\Core\Action; use Laminas\Diactoros\Response; use Laminas\Diactoros\ServerRequest; use Laminas\Diactoros\ServerRequestFactory; -use Mezzio\Router\RouterInterface; use PHPUnit\Framework\TestCase; use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; @@ -35,24 +34,11 @@ class QrCodeActionTest extends TestCase private const WHITE = 0xFFFFFF; private const BLACK = 0x0; - private QrCodeAction $action; private ObjectProphecy $urlResolver; - private QrCodeOptions $options; protected function setUp(): void { - $router = $this->prophesize(RouterInterface::class); - $router->generateUri(Argument::cetera())->willReturn('/foo/bar'); - $this->urlResolver = $this->prophesize(ShortUrlResolverInterface::class); - $this->options = new QrCodeOptions(); - - $this->action = new QrCodeAction( - $this->urlResolver->reveal(), - new ShortUrlStringifier(['domain' => 'doma.in']), - new NullLogger(), - $this->options, - ); } /** @test */ @@ -65,7 +51,7 @@ class QrCodeActionTest extends TestCase $delegate = $this->prophesize(RequestHandlerInterface::class); $process = $delegate->handle(Argument::any())->willReturn(new Response()); - $this->action->process((new ServerRequest())->withAttribute('shortCode', $shortCode), $delegate->reveal()); + $this->action()->process((new ServerRequest())->withAttribute('shortCode', $shortCode), $delegate->reveal()); $process->shouldHaveBeenCalledOnce(); } @@ -79,7 +65,7 @@ class QrCodeActionTest extends TestCase ->shouldBeCalledOnce(); $delegate = $this->prophesize(RequestHandlerInterface::class); - $resp = $this->action->process( + $resp = $this->action()->process( (new ServerRequest())->withAttribute('shortCode', $shortCode), $delegate->reveal(), ); @@ -98,7 +84,6 @@ class QrCodeActionTest extends TestCase array $query, string $expectedContentType, ): void { - $this->options->setFromArray(['format' => $defaultFormat]); $code = 'abc123'; $this->urlResolver->resolveEnabledShortUrl(ShortUrlIdentifier::fromShortCodeAndDomain($code, ''))->willReturn( ShortUrl::createEmpty(), @@ -106,7 +91,7 @@ class QrCodeActionTest extends TestCase $delegate = $this->prophesize(RequestHandlerInterface::class); $req = (new ServerRequest())->withAttribute('shortCode', $code)->withQueryParams($query); - $resp = $this->action->process($req, $delegate->reveal()); + $resp = $this->action(new QrCodeOptions(format: $defaultFormat))->process($req, $delegate->reveal()); self::assertEquals($expectedContentType, $resp->getHeaderLine('Content-Type')); } @@ -128,18 +113,17 @@ class QrCodeActionTest extends TestCase * @dataProvider provideRequestsWithSize */ public function imageIsReturnedWithExpectedSize( - array $defaults, + QrCodeOptions $defaultOptions, ServerRequestInterface $req, int $expectedSize, ): void { - $this->options->setFromArray($defaults); $code = 'abc123'; $this->urlResolver->resolveEnabledShortUrl(ShortUrlIdentifier::fromShortCodeAndDomain($code, ''))->willReturn( ShortUrl::createEmpty(), ); $delegate = $this->prophesize(RequestHandlerInterface::class); - $resp = $this->action->process($req->withAttribute('shortCode', $code), $delegate->reveal()); + $resp = $this->action($defaultOptions)->process($req->withAttribute('shortCode', $code), $delegate->reveal()); [$size] = getimagesizefromstring($resp->getBody()->__toString()); self::assertEquals($expectedSize, $size); @@ -148,52 +132,64 @@ class QrCodeActionTest extends TestCase public function provideRequestsWithSize(): iterable { yield 'different margin and size defaults' => [ - ['size' => 660, 'margin' => 40], + new QrCodeOptions(size: 660, margin: 40), ServerRequestFactory::fromGlobals(), 740, ]; - yield 'no size' => [[], ServerRequestFactory::fromGlobals(), 300]; - yield 'no size, different default' => [['size' => 500], ServerRequestFactory::fromGlobals(), 500]; - yield 'size in query' => [[], ServerRequestFactory::fromGlobals()->withQueryParams(['size' => '123']), 123]; + yield 'no size' => [new QrCodeOptions(), ServerRequestFactory::fromGlobals(), 300]; + yield 'no size, different default' => [new QrCodeOptions(size: 500), ServerRequestFactory::fromGlobals(), 500]; + yield 'size in query' => [ + new QrCodeOptions(), + ServerRequestFactory::fromGlobals()->withQueryParams(['size' => '123']), + 123, + ]; yield 'size in query, default margin' => [ - ['margin' => 25], + new QrCodeOptions(margin: 25), ServerRequestFactory::fromGlobals()->withQueryParams(['size' => '123']), 173, ]; - yield 'margin' => [[], ServerRequestFactory::fromGlobals()->withQueryParams(['margin' => '35']), 370]; + yield 'margin' => [ + new QrCodeOptions(), + ServerRequestFactory::fromGlobals()->withQueryParams(['margin' => '35']), + 370, + ]; yield 'margin and different default' => [ - ['size' => 400], + new QrCodeOptions(size: 400), ServerRequestFactory::fromGlobals()->withQueryParams(['margin' => '35']), 470, ]; yield 'margin and size' => [ - [], + new QrCodeOptions(), ServerRequestFactory::fromGlobals()->withQueryParams(['margin' => '100', 'size' => '200']), 400, ]; - yield 'negative margin' => [[], ServerRequestFactory::fromGlobals()->withQueryParams(['margin' => '-50']), 300]; + yield 'negative margin' => [ + new QrCodeOptions(), + ServerRequestFactory::fromGlobals()->withQueryParams(['margin' => '-50']), + 300, + ]; yield 'negative margin, default margin' => [ - ['margin' => 10], + new QrCodeOptions(margin: 10), ServerRequestFactory::fromGlobals()->withQueryParams(['margin' => '-50']), 300, ]; yield 'non-numeric margin' => [ - [], + new QrCodeOptions(), ServerRequestFactory::fromGlobals()->withQueryParams(['margin' => 'foo']), 300, ]; yield 'negative margin and size' => [ - [], + new QrCodeOptions(), ServerRequestFactory::fromGlobals()->withQueryParams(['margin' => '-1', 'size' => '150']), 150, ]; yield 'negative margin and size, default margin' => [ - ['margin' => 5], + new QrCodeOptions(margin: 5), ServerRequestFactory::fromGlobals()->withQueryParams(['margin' => '-1', 'size' => '150']), 150, ]; yield 'non-numeric margin and size' => [ - [], + new QrCodeOptions(), ServerRequestFactory::fromGlobals()->withQueryParams(['margin' => 'foo', 'size' => '538']), 538, ]; @@ -204,11 +200,10 @@ class QrCodeActionTest extends TestCase * @dataProvider provideRoundBlockSize */ public function imageCanRemoveExtraMarginWhenBlockRoundIsDisabled( - array $defaults, + QrCodeOptions $defaultOptions, ?string $roundBlockSize, int $expectedColor, ): void { - $this->options->setFromArray($defaults); $code = 'abc123'; $req = ServerRequestFactory::fromGlobals() ->withQueryParams(['size' => 250, 'roundBlockSize' => $roundBlockSize]) @@ -219,7 +214,7 @@ class QrCodeActionTest extends TestCase ); $delegate = $this->prophesize(RequestHandlerInterface::class); - $resp = $this->action->process($req, $delegate->reveal()); + $resp = $this->action($defaultOptions)->process($req, $delegate->reveal()); $image = imagecreatefromstring($resp->getBody()->__toString()); $color = imagecolorat($image, 1, 1); @@ -228,11 +223,33 @@ class QrCodeActionTest extends TestCase public function provideRoundBlockSize(): iterable { - yield 'no round block param' => [[], null, self::WHITE]; - yield 'no round block param, but disabled by default' => [['round_block_size' => false], null, self::BLACK]; - yield 'round block: "true"' => [[], 'true', self::WHITE]; - yield 'round block: "true", but disabled by default' => [['round_block_size' => false], 'true', self::WHITE]; - yield 'round block: "false"' => [[], 'false', self::BLACK]; - yield 'round block: "false", but enabled by default' => [['round_block_size' => true], 'false', self::BLACK]; + yield 'no round block param' => [new QrCodeOptions(), null, self::WHITE]; + yield 'no round block param, but disabled by default' => [ + new QrCodeOptions(roundBlockSize: false), + null, + self::BLACK, + ]; + yield 'round block: "true"' => [new QrCodeOptions(), 'true', self::WHITE]; + yield 'round block: "true", but disabled by default' => [ + new QrCodeOptions(roundBlockSize: false), + 'true', + self::WHITE, + ]; + yield 'round block: "false"' => [new QrCodeOptions(), 'false', self::BLACK]; + yield 'round block: "false", but enabled by default' => [ + new QrCodeOptions(roundBlockSize: true), + 'false', + self::BLACK, + ]; + } + + public function action(?QrCodeOptions $options = null): QrCodeAction + { + return new QrCodeAction( + $this->urlResolver->reveal(), + new ShortUrlStringifier(['domain' => 'doma.in']), + new NullLogger(), + $options ?? new QrCodeOptions(), + ); } } From 8f680788358781a08b7a6afd698666e33eadaa06 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 17 Sep 2022 13:56:59 +0200 Subject: [PATCH 094/108] Migrated RabbitMqOptions to immutable object --- module/Core/config/dependencies.config.php | 3 +- .../RabbitMq/NotifyNewShortUrlToRabbitMq.php | 2 +- .../RabbitMq/NotifyVisitToRabbitMq.php | 4 +- module/Core/src/Options/RabbitMqOptions.php | 37 +++--------------- .../NotifyNewShortUrlToRabbitMqTest.php | 32 ++++++++-------- .../RabbitMq/NotifyVisitToRabbitMqTest.php | 38 +++++++++---------- 6 files changed, 42 insertions(+), 74 deletions(-) diff --git a/module/Core/config/dependencies.config.php b/module/Core/config/dependencies.config.php index df63eeff..8c06001e 100644 --- a/module/Core/config/dependencies.config.php +++ b/module/Core/config/dependencies.config.php @@ -28,7 +28,7 @@ return [ Options\UrlShortenerOptions::class => ConfigAbstractFactory::class, Options\TrackingOptions::class => [ValinorConfigFactory::class, 'config.tracking'], Options\QrCodeOptions::class => [ValinorConfigFactory::class, 'config.qr_codes'], - Options\RabbitMqOptions::class => ConfigAbstractFactory::class, + Options\RabbitMqOptions::class => [ValinorConfigFactory::class, 'config.rabbitmq'], Options\WebhookOptions::class => ConfigAbstractFactory::class, Service\UrlShortener::class => ConfigAbstractFactory::class, @@ -88,7 +88,6 @@ return [ Options\RedirectOptions::class => ['config.redirects'], Options\UrlShortenerOptions::class => ['config.url_shortener'], - Options\RabbitMqOptions::class => ['config.rabbitmq'], Options\WebhookOptions::class => ['config.visits_webhooks'], Service\UrlShortener::class => [ diff --git a/module/Core/src/EventDispatcher/RabbitMq/NotifyNewShortUrlToRabbitMq.php b/module/Core/src/EventDispatcher/RabbitMq/NotifyNewShortUrlToRabbitMq.php index 488247d7..daa7cafb 100644 --- a/module/Core/src/EventDispatcher/RabbitMq/NotifyNewShortUrlToRabbitMq.php +++ b/module/Core/src/EventDispatcher/RabbitMq/NotifyNewShortUrlToRabbitMq.php @@ -26,7 +26,7 @@ class NotifyNewShortUrlToRabbitMq extends AbstractNotifyNewShortUrlListener protected function isEnabled(): bool { - return $this->options->isEnabled(); + return $this->options->enabled; } protected function getRemoteSystem(): RemoteSystem diff --git a/module/Core/src/EventDispatcher/RabbitMq/NotifyVisitToRabbitMq.php b/module/Core/src/EventDispatcher/RabbitMq/NotifyVisitToRabbitMq.php index 0faa795c..989de0a5 100644 --- a/module/Core/src/EventDispatcher/RabbitMq/NotifyVisitToRabbitMq.php +++ b/module/Core/src/EventDispatcher/RabbitMq/NotifyVisitToRabbitMq.php @@ -35,7 +35,7 @@ class NotifyVisitToRabbitMq extends AbstractNotifyVisitListener protected function determineUpdatesForVisit(Visit $visit): array { // Once the two deprecated cases below have been removed, make parent method private - if (! $this->options->legacyVisitsPublishing()) { + if (! $this->options->legacyVisitsPublishing) { return parent::determineUpdatesForVisit($visit); } @@ -61,7 +61,7 @@ class NotifyVisitToRabbitMq extends AbstractNotifyVisitListener protected function isEnabled(): bool { - return $this->options->isEnabled(); + return $this->options->enabled; } protected function getRemoteSystem(): RemoteSystem diff --git a/module/Core/src/Options/RabbitMqOptions.php b/module/Core/src/Options/RabbitMqOptions.php index 388cd2ea..cc25f3bf 100644 --- a/module/Core/src/Options/RabbitMqOptions.php +++ b/module/Core/src/Options/RabbitMqOptions.php @@ -4,37 +4,12 @@ declare(strict_types=1); namespace Shlinkio\Shlink\Core\Options; -use Laminas\Stdlib\AbstractOptions; - -class RabbitMqOptions extends AbstractOptions +final class RabbitMqOptions { - protected $__strictMode__ = false; // phpcs:ignore - - private bool $enabled = false; - /** @deprecated */ - private bool $legacyVisitsPublishing = false; - - public function isEnabled(): bool - { - return $this->enabled; - } - - protected function setEnabled(bool $enabled): self - { - $this->enabled = $enabled; - return $this; - } - - /** @deprecated */ - public function legacyVisitsPublishing(): bool - { - return $this->legacyVisitsPublishing; - } - - /** @deprecated */ - protected function setLegacyVisitsPublishing(bool $legacyVisitsPublishing): self - { - $this->legacyVisitsPublishing = $legacyVisitsPublishing; - return $this; + public function __construct( + public readonly bool $enabled = false, + /** @deprecated */ + public readonly bool $legacyVisitsPublishing = false, + ) { } } diff --git a/module/Core/test/EventDispatcher/RabbitMq/NotifyNewShortUrlToRabbitMqTest.php b/module/Core/test/EventDispatcher/RabbitMq/NotifyNewShortUrlToRabbitMqTest.php index 9cf44977..5365fe0e 100644 --- a/module/Core/test/EventDispatcher/RabbitMq/NotifyNewShortUrlToRabbitMqTest.php +++ b/module/Core/test/EventDispatcher/RabbitMq/NotifyNewShortUrlToRabbitMqTest.php @@ -27,12 +27,10 @@ class NotifyNewShortUrlToRabbitMqTest extends TestCase { use ProphecyTrait; - private NotifyNewShortUrlToRabbitMq $listener; private ObjectProphecy $helper; private ObjectProphecy $updatesGenerator; private ObjectProphecy $em; private ObjectProphecy $logger; - private RabbitMqOptions $options; protected function setUp(): void { @@ -40,23 +38,12 @@ class NotifyNewShortUrlToRabbitMqTest extends TestCase $this->updatesGenerator = $this->prophesize(PublishingUpdatesGeneratorInterface::class); $this->em = $this->prophesize(EntityManagerInterface::class); $this->logger = $this->prophesize(LoggerInterface::class); - $this->options = new RabbitMqOptions(['enabled' => true]); - - $this->listener = new NotifyNewShortUrlToRabbitMq( - $this->helper->reveal(), - $this->updatesGenerator->reveal(), - $this->em->reveal(), - $this->logger->reveal(), - $this->options, - ); } /** @test */ public function doesNothingWhenTheFeatureIsNotEnabled(): void { - $this->options->enabled = false; - - ($this->listener)(new ShortUrlCreated('123')); + ($this->listener(false))(new ShortUrlCreated('123')); $this->em->find(Argument::cetera())->shouldNotHaveBeenCalled(); $this->logger->warning(Argument::cetera())->shouldNotHaveBeenCalled(); @@ -74,7 +61,7 @@ class NotifyNewShortUrlToRabbitMqTest extends TestCase ['shortUrlId' => $shortUrlId, 'name' => 'RabbitMQ'], ); - ($this->listener)(new ShortUrlCreated($shortUrlId)); + ($this->listener())(new ShortUrlCreated($shortUrlId)); $find->shouldHaveBeenCalledOnce(); $logWarning->shouldHaveBeenCalledOnce(); @@ -92,7 +79,7 @@ class NotifyNewShortUrlToRabbitMqTest extends TestCase $update, ); - ($this->listener)(new ShortUrlCreated($shortUrlId)); + ($this->listener())(new ShortUrlCreated($shortUrlId)); $find->shouldHaveBeenCalledOnce(); $generateUpdate->shouldHaveBeenCalledOnce(); @@ -114,7 +101,7 @@ class NotifyNewShortUrlToRabbitMqTest extends TestCase ); $publish = $this->helper->publishUpdate($update)->willThrow($e); - ($this->listener)(new ShortUrlCreated($shortUrlId)); + ($this->listener())(new ShortUrlCreated($shortUrlId)); $this->logger->debug( 'Error while trying to notify {name} with new short URL. {e}', @@ -131,4 +118,15 @@ class NotifyNewShortUrlToRabbitMqTest extends TestCase yield [new Exception('Exception Error')]; yield [new DomainException('DomainException Error')]; } + + private function listener(bool $enabled = true): NotifyNewShortUrlToRabbitMq + { + return new NotifyNewShortUrlToRabbitMq( + $this->helper->reveal(), + $this->updatesGenerator->reveal(), + $this->em->reveal(), + $this->logger->reveal(), + new RabbitMqOptions($enabled), + ); + } } diff --git a/module/Core/test/EventDispatcher/RabbitMq/NotifyVisitToRabbitMqTest.php b/module/Core/test/EventDispatcher/RabbitMq/NotifyVisitToRabbitMqTest.php index 05ee7568..59f9c26a 100644 --- a/module/Core/test/EventDispatcher/RabbitMq/NotifyVisitToRabbitMqTest.php +++ b/module/Core/test/EventDispatcher/RabbitMq/NotifyVisitToRabbitMqTest.php @@ -35,12 +35,10 @@ class NotifyVisitToRabbitMqTest extends TestCase { use ProphecyTrait; - private NotifyVisitToRabbitMq $listener; private ObjectProphecy $helper; private ObjectProphecy $updatesGenerator; private ObjectProphecy $em; private ObjectProphecy $logger; - private RabbitMqOptions $options; protected function setUp(): void { @@ -48,24 +46,12 @@ class NotifyVisitToRabbitMqTest extends TestCase $this->updatesGenerator = $this->prophesize(PublishingUpdatesGeneratorInterface::class); $this->em = $this->prophesize(EntityManagerInterface::class); $this->logger = $this->prophesize(LoggerInterface::class); - $this->options = new RabbitMqOptions(['enabled' => true, 'legacy_visits_publishing' => false]); - - $this->listener = new NotifyVisitToRabbitMq( - $this->helper->reveal(), - $this->updatesGenerator->reveal(), - $this->em->reveal(), - $this->logger->reveal(), - new OrphanVisitDataTransformer(), - $this->options, - ); } /** @test */ public function doesNothingWhenTheFeatureIsNotEnabled(): void { - $this->options->enabled = false; - - ($this->listener)(new VisitLocated('123')); + ($this->listener(new RabbitMqOptions(enabled: false)))(new VisitLocated('123')); $this->em->find(Argument::cetera())->shouldNotHaveBeenCalled(); $this->logger->warning(Argument::cetera())->shouldNotHaveBeenCalled(); @@ -83,7 +69,7 @@ class NotifyVisitToRabbitMqTest extends TestCase ['visitId' => $visitId, 'name' => 'RabbitMQ'], ); - ($this->listener)(new VisitLocated($visitId)); + ($this->listener())(new VisitLocated($visitId)); $findVisit->shouldHaveBeenCalledOnce(); $logWarning->shouldHaveBeenCalledOnce(); @@ -105,7 +91,7 @@ class NotifyVisitToRabbitMqTest extends TestCase )->shouldBeCalledOnce(); }); - ($this->listener)(new VisitLocated($visitId)); + ($this->listener())(new VisitLocated($visitId)); $findVisit->shouldHaveBeenCalledOnce(); $this->helper->publishUpdate(Argument::type(Update::class))->shouldHaveBeenCalledTimes( @@ -144,7 +130,7 @@ class NotifyVisitToRabbitMqTest extends TestCase ); $publish = $this->helper->publishUpdate(Argument::cetera())->willThrow($e); - ($this->listener)(new VisitLocated($visitId)); + ($this->listener())(new VisitLocated($visitId)); $this->logger->debug( 'Error while trying to notify {name} with new visit. {e}', @@ -172,13 +158,11 @@ class NotifyVisitToRabbitMqTest extends TestCase callable $assert, callable $setup, ): void { - $this->options->legacyVisitsPublishing = $legacy; - $visitId = '123'; $findVisit = $this->em->find(Visit::class, $visitId)->willReturn($visit); $setup($this->updatesGenerator); - ($this->listener)(new VisitLocated($visitId)); + ($this->listener(new RabbitMqOptions(true, $legacy)))(new VisitLocated($visitId)); $findVisit->shouldHaveBeenCalledOnce(); $assert($this->helper, $this->updatesGenerator); @@ -247,4 +231,16 @@ class NotifyVisitToRabbitMqTest extends TestCase }, ]; } + + private function listener(?RabbitMqOptions $options = null): NotifyVisitToRabbitMq + { + return new NotifyVisitToRabbitMq( + $this->helper->reveal(), + $this->updatesGenerator->reveal(), + $this->em->reveal(), + $this->logger->reveal(), + new OrphanVisitDataTransformer(), + $options ?? new RabbitMqOptions(enabled: true, legacyVisitsPublishing: false), + ); + } } From 42af057316cef4e85e2d157b4d65f7da1076a664 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 17 Sep 2022 15:36:40 +0200 Subject: [PATCH 095/108] Migrated RedirectOptions to immutable object --- module/Core/config/dependencies.config.php | 3 +- module/Core/src/Options/RedirectOptions.php | 37 +++++-------------- .../Core/src/Util/RedirectResponseHelper.php | 4 +- .../test/Util/RedirectResponseHelperTest.php | 19 ++++------ 4 files changed, 20 insertions(+), 43 deletions(-) diff --git a/module/Core/config/dependencies.config.php b/module/Core/config/dependencies.config.php index 8c06001e..79192c16 100644 --- a/module/Core/config/dependencies.config.php +++ b/module/Core/config/dependencies.config.php @@ -24,7 +24,7 @@ return [ Options\AppOptions::class => [ValinorConfigFactory::class, 'config.app_options'], Options\DeleteShortUrlsOptions::class => [ValinorConfigFactory::class, 'config.delete_short_urls'], Options\NotFoundRedirectOptions::class => [ValinorConfigFactory::class, 'config.not_found_redirects'], - Options\RedirectOptions::class => ConfigAbstractFactory::class, + Options\RedirectOptions::class => [ValinorConfigFactory::class, 'config.redirects'], Options\UrlShortenerOptions::class => ConfigAbstractFactory::class, Options\TrackingOptions::class => [ValinorConfigFactory::class, 'config.tracking'], Options\QrCodeOptions::class => [ValinorConfigFactory::class, 'config.qr_codes'], @@ -86,7 +86,6 @@ return [ Domain\DomainService::class, ], - Options\RedirectOptions::class => ['config.redirects'], Options\UrlShortenerOptions::class => ['config.url_shortener'], Options\WebhookOptions::class => ['config.visits_webhooks'], diff --git a/module/Core/src/Options/RedirectOptions.php b/module/Core/src/Options/RedirectOptions.php index 5479c59b..9a1fedac 100644 --- a/module/Core/src/Options/RedirectOptions.php +++ b/module/Core/src/Options/RedirectOptions.php @@ -4,40 +4,23 @@ declare(strict_types=1); namespace Shlinkio\Shlink\Core\Options; -use Laminas\Stdlib\AbstractOptions; - use function Functional\contains; use const Shlinkio\Shlink\DEFAULT_REDIRECT_CACHE_LIFETIME; use const Shlinkio\Shlink\DEFAULT_REDIRECT_STATUS_CODE; -class RedirectOptions extends AbstractOptions +final class RedirectOptions { - private int $redirectStatusCode = DEFAULT_REDIRECT_STATUS_CODE; - private int $redirectCacheLifetime = DEFAULT_REDIRECT_CACHE_LIFETIME; + public readonly int $redirectStatusCode; + public readonly int $redirectCacheLifetime; - public function redirectStatusCode(): int - { - return $this->redirectStatusCode; - } - - protected function setRedirectStatusCode(int $redirectStatusCode): void - { - $this->redirectStatusCode = $this->normalizeRedirectStatusCode($redirectStatusCode); - } - - private function normalizeRedirectStatusCode(int $statusCode): int - { - return contains([301, 302], $statusCode) ? $statusCode : DEFAULT_REDIRECT_STATUS_CODE; - } - - public function redirectCacheLifetime(): int - { - return $this->redirectCacheLifetime; - } - - protected function setRedirectCacheLifetime(int $redirectCacheLifetime): void - { + public function __construct( + int $redirectStatusCode = DEFAULT_REDIRECT_STATUS_CODE, + int $redirectCacheLifetime = DEFAULT_REDIRECT_CACHE_LIFETIME, + ) { + $this->redirectStatusCode = contains([301, 302], $redirectStatusCode) + ? $redirectStatusCode + : DEFAULT_REDIRECT_STATUS_CODE; $this->redirectCacheLifetime = $redirectCacheLifetime > 0 ? $redirectCacheLifetime : DEFAULT_REDIRECT_CACHE_LIFETIME; diff --git a/module/Core/src/Util/RedirectResponseHelper.php b/module/Core/src/Util/RedirectResponseHelper.php index 312c2a95..dfc87480 100644 --- a/module/Core/src/Util/RedirectResponseHelper.php +++ b/module/Core/src/Util/RedirectResponseHelper.php @@ -19,9 +19,9 @@ class RedirectResponseHelper implements RedirectResponseHelperInterface public function buildRedirectResponse(string $location): ResponseInterface { - $statusCode = $this->options->redirectStatusCode(); + $statusCode = $this->options->redirectStatusCode; $headers = $statusCode === StatusCodeInterface::STATUS_FOUND ? [] : [ - 'Cache-Control' => sprintf('private,max-age=%s', $this->options->redirectCacheLifetime()), + 'Cache-Control' => sprintf('private,max-age=%s', $this->options->redirectCacheLifetime), ]; return new RedirectResponse($location, $statusCode, $headers); diff --git a/module/Core/test/Util/RedirectResponseHelperTest.php b/module/Core/test/Util/RedirectResponseHelperTest.php index 651d4bc7..fc2b89a2 100644 --- a/module/Core/test/Util/RedirectResponseHelperTest.php +++ b/module/Core/test/Util/RedirectResponseHelperTest.php @@ -11,15 +11,6 @@ use Shlinkio\Shlink\Core\Util\RedirectResponseHelper; class RedirectResponseHelperTest extends TestCase { - private RedirectResponseHelper $helper; - private RedirectOptions $shortenerOpts; - - protected function setUp(): void - { - $this->shortenerOpts = new RedirectOptions(); - $this->helper = new RedirectResponseHelper($this->shortenerOpts); - } - /** * @test * @dataProvider provideRedirectConfigs @@ -30,10 +21,9 @@ class RedirectResponseHelperTest extends TestCase int $expectedStatus, ?string $expectedCacheControl, ): void { - $this->shortenerOpts->redirectStatusCode = $configuredStatus; - $this->shortenerOpts->redirectCacheLifetime = $configuredLifetime; + $options = new RedirectOptions($configuredStatus, $configuredLifetime); - $response = $this->helper->buildRedirectResponse('destination'); + $response = $this->helper($options)->buildRedirectResponse('destination'); self::assertInstanceOf(RedirectResponse::class, $response); self::assertEquals($expectedStatus, $response->getStatusCode()); @@ -52,4 +42,9 @@ class RedirectResponseHelperTest extends TestCase yield 'status 301 with zero expiration' => [301, 0, 301, 'private,max-age=30']; yield 'status 301 with negative expiration' => [301, -20, 301, 'private,max-age=30']; } + + private function helper(?RedirectOptions $options = null): RedirectResponseHelper + { + return new RedirectResponseHelper($options ?? new RedirectOptions()); + } } From 8d244c8d340da64c8c8f5870fa278ef30e84f603 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 17 Sep 2022 15:54:43 +0200 Subject: [PATCH 096/108] Migrated UrlShortenerOptions to immutable object --- config/test/test_config.global.php | 1 - .../ShortUrl/CreateShortUrlCommand.php | 6 +- .../ShortUrl/CreateShortUrlCommandTest.php | 2 +- module/Core/config/dependencies.config.php | 3 +- .../Core/src/Options/UrlShortenerOptions.php | 70 +++---------------- .../ExtraPathRedirectMiddleware.php | 8 +-- module/Core/src/Util/UrlValidator.php | 4 +- .../ExtraPathRedirectMiddlewareTest.php | 39 ++++++----- module/Core/test/Util/UrlValidatorTest.php | 36 +++++----- .../Action/ShortUrl/CreateShortUrlAction.php | 2 +- 10 files changed, 58 insertions(+), 113 deletions(-) diff --git a/config/test/test_config.global.php b/config/test/test_config.global.php index 9b338d7a..678e1b05 100644 --- a/config/test/test_config.global.php +++ b/config/test/test_config.global.php @@ -130,7 +130,6 @@ return [ 'schema' => 'http', 'hostname' => 'doma.in', ], - 'validate_url' => true, ], 'mezzio-swoole' => [ diff --git a/module/CLI/src/Command/ShortUrl/CreateShortUrlCommand.php b/module/CLI/src/Command/ShortUrl/CreateShortUrlCommand.php index 6b4cce1a..666dea5b 100644 --- a/module/CLI/src/Command/ShortUrl/CreateShortUrlCommand.php +++ b/module/CLI/src/Command/ShortUrl/CreateShortUrlCommand.php @@ -40,7 +40,7 @@ class CreateShortUrlCommand extends Command private readonly UrlShortenerOptions $options, ) { parent::__construct(); - $this->defaultDomain = $this->options->domain()['hostname'] ?? ''; + $this->defaultDomain = $this->options->domain['hostname'] ?? ''; } protected function configure(): void @@ -158,7 +158,7 @@ class CreateShortUrlCommand extends Command $tags = unique(flatten(array_map($explodeWithComma, $input->getOption('tags')))); $customSlug = $input->getOption('custom-slug'); $maxVisits = $input->getOption('max-visits'); - $shortCodeLength = $input->getOption('short-code-length') ?? $this->options->defaultShortCodesLength(); + $shortCodeLength = $input->getOption('short-code-length') ?? $this->options->defaultShortCodesLength; $doValidateUrl = $input->getOption('validate-url'); try { @@ -175,7 +175,7 @@ class CreateShortUrlCommand extends Command ShortUrlInputFilter::TAGS => $tags, ShortUrlInputFilter::CRAWLABLE => $input->getOption('crawlable'), ShortUrlInputFilter::FORWARD_QUERY => !$input->getOption('no-forward-query'), - EnvVars::MULTI_SEGMENT_SLUGS_ENABLED->value => $this->options->multiSegmentSlugsEnabled(), + EnvVars::MULTI_SEGMENT_SLUGS_ENABLED->value => $this->options->multiSegmentSlugsEnabled, ])); $io->writeln([ diff --git a/module/CLI/test/Command/ShortUrl/CreateShortUrlCommandTest.php b/module/CLI/test/Command/ShortUrl/CreateShortUrlCommandTest.php index e529f0ad..733f6b72 100644 --- a/module/CLI/test/Command/ShortUrl/CreateShortUrlCommandTest.php +++ b/module/CLI/test/Command/ShortUrl/CreateShortUrlCommandTest.php @@ -39,7 +39,7 @@ class CreateShortUrlCommandTest extends TestCase $command = new CreateShortUrlCommand( $this->urlShortener->reveal(), $this->stringifier->reveal(), - new UrlShortenerOptions(['defaultShortCodesLength' => 5, 'domain' => ['hostname' => self::DEFAULT_DOMAIN]]), + new UrlShortenerOptions(domain: ['hostname' => self::DEFAULT_DOMAIN], defaultShortCodesLength: 5), ); $this->commandTester = $this->testerForCommand($command); } diff --git a/module/Core/config/dependencies.config.php b/module/Core/config/dependencies.config.php index 79192c16..9855e2aa 100644 --- a/module/Core/config/dependencies.config.php +++ b/module/Core/config/dependencies.config.php @@ -25,7 +25,7 @@ return [ Options\DeleteShortUrlsOptions::class => [ValinorConfigFactory::class, 'config.delete_short_urls'], Options\NotFoundRedirectOptions::class => [ValinorConfigFactory::class, 'config.not_found_redirects'], Options\RedirectOptions::class => [ValinorConfigFactory::class, 'config.redirects'], - Options\UrlShortenerOptions::class => ConfigAbstractFactory::class, + Options\UrlShortenerOptions::class => [ValinorConfigFactory::class, 'config.url_shortener'], Options\TrackingOptions::class => [ValinorConfigFactory::class, 'config.tracking'], Options\QrCodeOptions::class => [ValinorConfigFactory::class, 'config.qr_codes'], Options\RabbitMqOptions::class => [ValinorConfigFactory::class, 'config.rabbitmq'], @@ -86,7 +86,6 @@ return [ Domain\DomainService::class, ], - Options\UrlShortenerOptions::class => ['config.url_shortener'], Options\WebhookOptions::class => ['config.visits_webhooks'], Service\UrlShortener::class => [ diff --git a/module/Core/src/Options/UrlShortenerOptions.php b/module/Core/src/Options/UrlShortenerOptions.php index 38e185c2..dd7fdc8d 100644 --- a/module/Core/src/Options/UrlShortenerOptions.php +++ b/module/Core/src/Options/UrlShortenerOptions.php @@ -4,69 +4,17 @@ declare(strict_types=1); namespace Shlinkio\Shlink\Core\Options; -use Laminas\Stdlib\AbstractOptions; - use const Shlinkio\Shlink\DEFAULT_SHORT_CODES_LENGTH; -class UrlShortenerOptions extends AbstractOptions +final class UrlShortenerOptions { - protected $__strictMode__ = false; // phpcs:ignore - - private array $domain = []; - private int $defaultShortCodesLength = DEFAULT_SHORT_CODES_LENGTH; - private bool $autoResolveTitles = false; - private bool $appendExtraPath = false; - private bool $multiSegmentSlugsEnabled = false; - - public function domain(): array - { - return $this->domain; - } - - protected function setDomain(array $domain): self - { - $this->domain = $domain; - return $this; - } - - public function defaultShortCodesLength(): int - { - return $this->defaultShortCodesLength; - } - - protected function setDefaultShortCodesLength(int $defaultShortCodesLength): self - { - $this->defaultShortCodesLength = $defaultShortCodesLength; - return $this; - } - - public function autoResolveTitles(): bool - { - return $this->autoResolveTitles; - } - - protected function setAutoResolveTitles(bool $autoResolveTitles): void - { - $this->autoResolveTitles = $autoResolveTitles; - } - - public function appendExtraPath(): bool - { - return $this->appendExtraPath; - } - - protected function setAppendExtraPath(bool $appendExtraPath): void - { - $this->appendExtraPath = $appendExtraPath; - } - - public function multiSegmentSlugsEnabled(): bool - { - return $this->multiSegmentSlugsEnabled; - } - - protected function setMultiSegmentSlugsEnabled(bool $multiSegmentSlugsEnabled): void - { - $this->multiSegmentSlugsEnabled = $multiSegmentSlugsEnabled; + public function __construct( + /** @var array{schema: ?string, hostname: ?string} */ + public readonly array $domain = ['schema' => null, 'hostname' => null], + public readonly int $defaultShortCodesLength = DEFAULT_SHORT_CODES_LENGTH, + public readonly bool $autoResolveTitles = false, + public readonly bool $appendExtraPath = false, + public readonly bool $multiSegmentSlugsEnabled = false, + ) { } } diff --git a/module/Core/src/ShortUrl/Middleware/ExtraPathRedirectMiddleware.php b/module/Core/src/ShortUrl/Middleware/ExtraPathRedirectMiddleware.php index bb350aa2..3fead5f2 100644 --- a/module/Core/src/ShortUrl/Middleware/ExtraPathRedirectMiddleware.php +++ b/module/Core/src/ShortUrl/Middleware/ExtraPathRedirectMiddleware.php @@ -49,16 +49,16 @@ class ExtraPathRedirectMiddleware implements MiddlewareInterface private function shouldApplyLogic(?NotFoundType $notFoundType): bool { - if ($notFoundType === null || ! $this->urlShortenerOptions->appendExtraPath()) { + if ($notFoundType === null || ! $this->urlShortenerOptions->appendExtraPath) { return false; } return ( // If multi-segment slugs are enabled, the appropriate not-found type is "invalid_short_url" - $this->urlShortenerOptions->multiSegmentSlugsEnabled() && $notFoundType->isInvalidShortUrl() + $this->urlShortenerOptions->multiSegmentSlugsEnabled && $notFoundType->isInvalidShortUrl() ) || ( // If multi-segment slugs are disabled, the appropriate not-found type is "regular_404" - ! $this->urlShortenerOptions->multiSegmentSlugsEnabled() && $notFoundType->isRegularNotFound() + ! $this->urlShortenerOptions->multiSegmentSlugsEnabled && $notFoundType->isRegularNotFound() ); } @@ -79,7 +79,7 @@ class ExtraPathRedirectMiddleware implements MiddlewareInterface $longUrl = $this->redirectionBuilder->buildShortUrlRedirect($shortUrl, $query, $extraPath); return $this->redirectResponseHelper->buildRedirectResponse($longUrl); } catch (ShortUrlNotFoundException) { - if ($extraPath === null || ! $this->urlShortenerOptions->multiSegmentSlugsEnabled()) { + if ($extraPath === null || ! $this->urlShortenerOptions->multiSegmentSlugsEnabled) { return $handler->handle($request); } diff --git a/module/Core/src/Util/UrlValidator.php b/module/Core/src/Util/UrlValidator.php index 2e2965b1..0057660a 100644 --- a/module/Core/src/Util/UrlValidator.php +++ b/module/Core/src/Util/UrlValidator.php @@ -46,11 +46,11 @@ class UrlValidator implements UrlValidatorInterface, RequestMethodInterface public function validateUrlWithTitle(string $url, bool $doValidate): ?string { - if (! $doValidate && ! $this->options->autoResolveTitles()) { + if (! $doValidate && ! $this->options->autoResolveTitles) { return null; } - if (! $this->options->autoResolveTitles()) { + if (! $this->options->autoResolveTitles) { $this->validateUrlAndGetResponse($url, self::METHOD_HEAD); return null; } diff --git a/module/Core/test/ShortUrl/Middleware/ExtraPathRedirectMiddlewareTest.php b/module/Core/test/ShortUrl/Middleware/ExtraPathRedirectMiddlewareTest.php index 4099faea..3cd2adef 100644 --- a/module/Core/test/ShortUrl/Middleware/ExtraPathRedirectMiddlewareTest.php +++ b/module/Core/test/ShortUrl/Middleware/ExtraPathRedirectMiddlewareTest.php @@ -34,12 +34,10 @@ class ExtraPathRedirectMiddlewareTest extends TestCase { use ProphecyTrait; - private ExtraPathRedirectMiddleware $middleware; private ObjectProphecy $resolver; private ObjectProphecy $requestTracker; private ObjectProphecy $redirectionBuilder; private ObjectProphecy $redirectResponseHelper; - private UrlShortenerOptions $options; private ObjectProphecy $handler; protected function setUp(): void @@ -48,16 +46,6 @@ class ExtraPathRedirectMiddlewareTest extends TestCase $this->requestTracker = $this->prophesize(RequestTrackerInterface::class); $this->redirectionBuilder = $this->prophesize(ShortUrlRedirectionBuilderInterface::class); $this->redirectResponseHelper = $this->prophesize(RedirectResponseHelperInterface::class); - $this->options = new UrlShortenerOptions(['append_extra_path' => true]); - - $this->middleware = new ExtraPathRedirectMiddleware( - $this->resolver->reveal(), - $this->requestTracker->reveal(), - $this->redirectionBuilder->reveal(), - $this->redirectResponseHelper->reveal(), - $this->options, - ); - $this->handler = $this->prophesize(RequestHandlerInterface::class); $this->handler->handle(Argument::cetera())->willReturn(new RedirectResponse('')); } @@ -71,10 +59,12 @@ class ExtraPathRedirectMiddlewareTest extends TestCase bool $multiSegmentEnabled, ServerRequestInterface $request, ): void { - $this->options->appendExtraPath = $appendExtraPath; - $this->options->multiSegmentSlugsEnabled = $multiSegmentEnabled; + $options = new UrlShortenerOptions( + appendExtraPath: $appendExtraPath, + multiSegmentSlugsEnabled: $multiSegmentEnabled, + ); - $this->middleware->process($request, $this->handler->reveal()); + $this->middleware($options)->process($request, $this->handler->reveal()); $this->handler->handle($request)->shouldHaveBeenCalledOnce(); $this->resolver->resolveEnabledShortUrl(Argument::cetera())->shouldNotHaveBeenCalled(); @@ -123,7 +113,7 @@ class ExtraPathRedirectMiddlewareTest extends TestCase bool $multiSegmentEnabled, int $expectedResolveCalls, ): void { - $this->options->multiSegmentSlugsEnabled = $multiSegmentEnabled; + $options = new UrlShortenerOptions(appendExtraPath: true, multiSegmentSlugsEnabled: $multiSegmentEnabled); $type = $this->prophesize(NotFoundType::class); $type->isRegularNotFound()->willReturn(true); @@ -135,7 +125,7 @@ class ExtraPathRedirectMiddlewareTest extends TestCase Argument::that(fn (ShortUrlIdentifier $identifier) => str_starts_with($identifier->shortCode, 'shortCode')), )->willThrow(ShortUrlNotFoundException::class); - $this->middleware->process($request, $this->handler->reveal()); + $this->middleware($options)->process($request, $this->handler->reveal()); $resolve->shouldHaveBeenCalledTimes($expectedResolveCalls); $this->requestTracker->trackIfApplicable(Argument::cetera())->shouldNotHaveBeenCalled(); @@ -152,7 +142,7 @@ class ExtraPathRedirectMiddlewareTest extends TestCase int $expectedResolveCalls, ?string $expectedExtraPath, ): void { - $this->options->multiSegmentSlugsEnabled = $multiSegmentEnabled; + $options = new UrlShortenerOptions(appendExtraPath: true, multiSegmentSlugsEnabled: $multiSegmentEnabled); $type = $this->prophesize(NotFoundType::class); $type->isRegularNotFound()->willReturn(true); @@ -181,7 +171,7 @@ class ExtraPathRedirectMiddlewareTest extends TestCase new RedirectResponse(''), ); - $this->middleware->process($request, $this->handler->reveal()); + $this->middleware($options)->process($request, $this->handler->reveal()); $resolve->shouldHaveBeenCalledTimes($expectedResolveCalls); $buildLongUrl->shouldHaveBeenCalledOnce(); @@ -194,4 +184,15 @@ class ExtraPathRedirectMiddlewareTest extends TestCase yield [false, 1, '/bar/baz']; yield [true, 3, null]; } + + private function middleware(?UrlShortenerOptions $options = null): ExtraPathRedirectMiddleware + { + return new ExtraPathRedirectMiddleware( + $this->resolver->reveal(), + $this->requestTracker->reveal(), + $this->redirectionBuilder->reveal(), + $this->redirectResponseHelper->reveal(), + $options ?? new UrlShortenerOptions(appendExtraPath: true), + ); + } } diff --git a/module/Core/test/Util/UrlValidatorTest.php b/module/Core/test/Util/UrlValidatorTest.php index de5cad23..cc13bd2c 100644 --- a/module/Core/test/Util/UrlValidatorTest.php +++ b/module/Core/test/Util/UrlValidatorTest.php @@ -23,15 +23,11 @@ class UrlValidatorTest extends TestCase { use ProphecyTrait; - private UrlValidator $urlValidator; private ObjectProphecy $httpClient; - private UrlShortenerOptions $options; protected function setUp(): void { $this->httpClient = $this->prophesize(ClientInterface::class); - $this->options = new UrlShortenerOptions(['validate_url' => true]); - $this->urlValidator = new UrlValidator($this->httpClient->reveal(), $this->options); } /** @test */ @@ -42,7 +38,7 @@ class UrlValidatorTest extends TestCase $request->shouldBeCalledOnce(); $this->expectException(InvalidUrlException::class); - $this->urlValidator->validateUrl('http://foobar.com/12345/hello?foo=bar', true); + $this->urlValidator()->validateUrl('http://foobar.com/12345/hello?foo=bar', true); } /** @test */ @@ -65,7 +61,7 @@ class UrlValidatorTest extends TestCase }), )->willReturn(new Response()); - $this->urlValidator->validateUrl($expectedUrl, true); + $this->urlValidator()->validateUrl($expectedUrl, true); $request->shouldHaveBeenCalledOnce(); } @@ -75,7 +71,7 @@ class UrlValidatorTest extends TestCase { $request = $this->httpClient->request(Argument::cetera())->willReturn(new Response()); - $this->urlValidator->validateUrl('', false); + $this->urlValidator()->validateUrl('', false); $request->shouldNotHaveBeenCalled(); } @@ -84,9 +80,8 @@ class UrlValidatorTest extends TestCase public function validateUrlWithTitleReturnsNullWhenRequestFailsAndValidationIsDisabled(): void { $request = $this->httpClient->request(Argument::cetera())->willThrow(ClientException::class); - $this->options->autoResolveTitles = true; - $result = $this->urlValidator->validateUrlWithTitle('http://foobar.com/12345/hello?foo=bar', false); + $result = $this->urlValidator(true)->validateUrlWithTitle('http://foobar.com/12345/hello?foo=bar', false); self::assertNull($result); $request->shouldHaveBeenCalledOnce(); @@ -96,9 +91,8 @@ class UrlValidatorTest extends TestCase public function validateUrlWithTitleReturnsNullWhenAutoResolutionIsDisabled(): void { $request = $this->httpClient->request(Argument::cetera())->willReturn($this->respWithTitle()); - $this->options->autoResolveTitles = false; - $result = $this->urlValidator->validateUrlWithTitle('http://foobar.com/12345/hello?foo=bar', false); + $result = $this->urlValidator()->validateUrlWithTitle('http://foobar.com/12345/hello?foo=bar', false); self::assertNull($result); $request->shouldNotHaveBeenCalled(); @@ -110,9 +104,8 @@ class UrlValidatorTest extends TestCase $request = $this->httpClient->request(RequestMethodInterface::METHOD_HEAD, Argument::cetera())->willReturn( $this->respWithTitle(), ); - $this->options->autoResolveTitles = false; - $result = $this->urlValidator->validateUrlWithTitle('http://foobar.com/12345/hello?foo=bar', true); + $result = $this->urlValidator()->validateUrlWithTitle('http://foobar.com/12345/hello?foo=bar', true); self::assertNull($result); $request->shouldHaveBeenCalledOnce(); @@ -124,9 +117,8 @@ class UrlValidatorTest extends TestCase $request = $this->httpClient->request(RequestMethodInterface::METHOD_GET, Argument::cetera())->willReturn( $this->respWithTitle(), ); - $this->options->autoResolveTitles = true; - $result = $this->urlValidator->validateUrlWithTitle('http://foobar.com/12345/hello?foo=bar', true); + $result = $this->urlValidator(true)->validateUrlWithTitle('http://foobar.com/12345/hello?foo=bar', true); self::assertEquals('Resolved "title"', $result); $request->shouldHaveBeenCalledOnce(); @@ -138,9 +130,8 @@ class UrlValidatorTest extends TestCase $request = $this->httpClient->request(RequestMethodInterface::METHOD_GET, Argument::cetera())->willReturn( new Response('php://memory', 200, ['Content-Type' => 'application/octet-stream']), ); - $this->options->autoResolveTitles = true; - $result = $this->urlValidator->validateUrlWithTitle('http://foobar.com/12345/hello?foo=bar', true); + $result = $this->urlValidator(true)->validateUrlWithTitle('http://foobar.com/12345/hello?foo=bar', true); self::assertNull($result); $request->shouldHaveBeenCalledOnce(); @@ -152,9 +143,8 @@ class UrlValidatorTest extends TestCase $request = $this->httpClient->request(RequestMethodInterface::METHOD_GET, Argument::cetera())->willReturn( new Response($this->createStreamWithContent('No title'), 200, ['Content-Type' => 'text/html']), ); - $this->options->autoResolveTitles = true; - $result = $this->urlValidator->validateUrlWithTitle('http://foobar.com/12345/hello?foo=bar', true); + $result = $this->urlValidator(true)->validateUrlWithTitle('http://foobar.com/12345/hello?foo=bar', true); self::assertNull($result); $request->shouldHaveBeenCalledOnce(); @@ -174,4 +164,12 @@ class UrlValidatorTest extends TestCase return $body; } + + public function urlValidator(bool $autoResolveTitles = false): UrlValidator + { + return new UrlValidator( + $this->httpClient->reveal(), + new UrlShortenerOptions(autoResolveTitles: $autoResolveTitles), + ); + } } diff --git a/module/Rest/src/Action/ShortUrl/CreateShortUrlAction.php b/module/Rest/src/Action/ShortUrl/CreateShortUrlAction.php index 376c6bec..46fff970 100644 --- a/module/Rest/src/Action/ShortUrl/CreateShortUrlAction.php +++ b/module/Rest/src/Action/ShortUrl/CreateShortUrlAction.php @@ -23,7 +23,7 @@ class CreateShortUrlAction extends AbstractCreateShortUrlAction { $payload = (array) $request->getParsedBody(); $payload[ShortUrlInputFilter::API_KEY] = AuthenticationMiddleware::apiKeyFromRequest($request); - $payload[EnvVars::MULTI_SEGMENT_SLUGS_ENABLED->value] = $this->urlShortenerOptions->multiSegmentSlugsEnabled(); + $payload[EnvVars::MULTI_SEGMENT_SLUGS_ENABLED->value] = $this->urlShortenerOptions->multiSegmentSlugsEnabled; return ShortUrlMeta::fromRawData($payload); } From 24088296270db3af2f6502703008e4e60fd6a433 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 17 Sep 2022 15:55:54 +0200 Subject: [PATCH 097/108] Updated changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f8d58fb8..f85af37f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this * [#1339](https://github.com/shlinkio/shlink/issues/1339) Added new test suite for CLI E2E tests. * [#1503](https://github.com/shlinkio/shlink/issues/1503) Drastically improved build time in GitHub Actions, by optimizing parallelization and adding php extensions cache. * [#1525](https://github.com/shlinkio/shlink/issues/1525) Migrated to custom doctrine CLI entry point. +* [#1492](https://github.com/shlinkio/shlink/issues/1492) Migrated to immutable options objects, mapped with [cuyz/valinor](https://github.com/CuyZ/Valinor). ### Deprecated * *Nothing* From 3f01fad12fcdd5c7037cd3c71e3dfcaea3623223 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 18 Sep 2022 09:29:38 +0200 Subject: [PATCH 098/108] Ensured empty initial PAI keys are discarded --- module/Rest/src/ApiKey/InitialApiKeyDelegator.php | 2 +- module/Rest/test/ApiKey/InitialApiKeyDelegatorTest.php | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/module/Rest/src/ApiKey/InitialApiKeyDelegator.php b/module/Rest/src/ApiKey/InitialApiKeyDelegator.php index 9129d7d3..a5aa9d33 100644 --- a/module/Rest/src/ApiKey/InitialApiKeyDelegator.php +++ b/module/Rest/src/ApiKey/InitialApiKeyDelegator.php @@ -15,7 +15,7 @@ class InitialApiKeyDelegator public function __invoke(ContainerInterface $container, string $serviceName, callable $callback): Application { $initialApiKey = $container->get('config')['initial_api_key'] ?? null; - if ($initialApiKey !== null) { + if (! empty($initialApiKey)) { $this->createInitialApiKey($initialApiKey, $container); } diff --git a/module/Rest/test/ApiKey/InitialApiKeyDelegatorTest.php b/module/Rest/test/ApiKey/InitialApiKeyDelegatorTest.php index 7614fc9d..1db53b80 100644 --- a/module/Rest/test/ApiKey/InitialApiKeyDelegatorTest.php +++ b/module/Rest/test/ApiKey/InitialApiKeyDelegatorTest.php @@ -54,8 +54,9 @@ class InitialApiKeyDelegatorTest extends TestCase public function provideConfigs(): iterable { - yield [[], 0]; - yield [['initial_api_key' => null], 0]; - yield [['initial_api_key' => 'the_initial_key'], 1]; + yield 'no api key' => [[], 0]; + yield 'null api key' => [['initial_api_key' => null], 0]; + yield 'empty api key' => [['initial_api_key' => ''], 0]; + yield 'valid api key' => [['initial_api_key' => 'the_initial_key'], 1]; } } From 59bcd627175d5f8e86da0e4be05c8aeb4e598fcf Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 18 Sep 2022 10:01:22 +0200 Subject: [PATCH 099/108] Moved Geolocation services to its own namespace inside CLI module --- module/CLI/config/dependencies.config.php | 6 +++--- module/CLI/src/Command/Visit/DownloadGeoLiteDbCommand.php | 2 +- module/CLI/src/{Util => GeoLite}/GeolocationDbUpdater.php | 2 +- .../src/{Util => GeoLite}/GeolocationDbUpdaterInterface.php | 2 +- .../CLI/test/Command/Visit/DownloadGeoLiteDbCommandTest.php | 2 +- .../CLI/test/{Util => GeoLite}/GeolocationDbUpdaterTest.php | 4 ++-- module/Core/config/event_dispatcher.config.php | 2 +- module/Core/src/Action/AbstractTrackingAction.php | 4 ++-- module/Core/src/EventDispatcher/UpdateGeoLiteDb.php | 2 +- module/Core/test/EventDispatcher/UpdateGeoLiteDbTest.php | 2 +- 10 files changed, 14 insertions(+), 14 deletions(-) rename module/CLI/src/{Util => GeoLite}/GeolocationDbUpdater.php (98%) rename module/CLI/src/{Util => GeoLite}/GeolocationDbUpdaterInterface.php (89%) rename module/CLI/test/{Util => GeoLite}/GeolocationDbUpdaterTest.php (98%) diff --git a/module/CLI/config/dependencies.config.php b/module/CLI/config/dependencies.config.php index 6920e839..38368746 100644 --- a/module/CLI/config/dependencies.config.php +++ b/module/CLI/config/dependencies.config.php @@ -35,7 +35,7 @@ return [ SymfonyCli\Helper\ProcessHelper::class => ProcessHelperFactory::class, PhpExecutableFinder::class => InvokableFactory::class, - Util\GeolocationDbUpdater::class => ConfigAbstractFactory::class, + GeoLite\GeolocationDbUpdater::class => ConfigAbstractFactory::class, Util\ProcessRunner::class => ConfigAbstractFactory::class, ApiKey\RoleResolver::class => ConfigAbstractFactory::class, @@ -70,7 +70,7 @@ return [ ], ConfigAbstractFactory::class => [ - Util\GeolocationDbUpdater::class => [ + GeoLite\GeolocationDbUpdater::class => [ DbUpdater::class, Reader::class, LOCAL_LOCK_FACTORY, @@ -92,7 +92,7 @@ return [ Command\ShortUrl\GetShortUrlVisitsCommand::class => [Visit\VisitsStatsHelper::class], Command\ShortUrl\DeleteShortUrlCommand::class => [Service\ShortUrl\DeleteShortUrlService::class], - Command\Visit\DownloadGeoLiteDbCommand::class => [Util\GeolocationDbUpdater::class], + Command\Visit\DownloadGeoLiteDbCommand::class => [GeoLite\GeolocationDbUpdater::class], Command\Visit\LocateVisitsCommand::class => [ Visit\VisitLocator::class, IpLocationResolverInterface::class, diff --git a/module/CLI/src/Command/Visit/DownloadGeoLiteDbCommand.php b/module/CLI/src/Command/Visit/DownloadGeoLiteDbCommand.php index 41fb5f8d..c4384d33 100644 --- a/module/CLI/src/Command/Visit/DownloadGeoLiteDbCommand.php +++ b/module/CLI/src/Command/Visit/DownloadGeoLiteDbCommand.php @@ -5,8 +5,8 @@ declare(strict_types=1); namespace Shlinkio\Shlink\CLI\Command\Visit; use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException; +use Shlinkio\Shlink\CLI\GeoLite\GeolocationDbUpdaterInterface; use Shlinkio\Shlink\CLI\Util\ExitCodes; -use Shlinkio\Shlink\CLI\Util\GeolocationDbUpdaterInterface; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Helper\ProgressBar; use Symfony\Component\Console\Input\InputInterface; diff --git a/module/CLI/src/Util/GeolocationDbUpdater.php b/module/CLI/src/GeoLite/GeolocationDbUpdater.php similarity index 98% rename from module/CLI/src/Util/GeolocationDbUpdater.php rename to module/CLI/src/GeoLite/GeolocationDbUpdater.php index 913ad438..8061c1a0 100644 --- a/module/CLI/src/Util/GeolocationDbUpdater.php +++ b/module/CLI/src/GeoLite/GeolocationDbUpdater.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Shlinkio\Shlink\CLI\Util; +namespace Shlinkio\Shlink\CLI\GeoLite; use Cake\Chronos\Chronos; use GeoIp2\Database\Reader; diff --git a/module/CLI/src/Util/GeolocationDbUpdaterInterface.php b/module/CLI/src/GeoLite/GeolocationDbUpdaterInterface.php similarity index 89% rename from module/CLI/src/Util/GeolocationDbUpdaterInterface.php rename to module/CLI/src/GeoLite/GeolocationDbUpdaterInterface.php index 714f6a11..f7d5ecba 100644 --- a/module/CLI/src/Util/GeolocationDbUpdaterInterface.php +++ b/module/CLI/src/GeoLite/GeolocationDbUpdaterInterface.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Shlinkio\Shlink\CLI\Util; +namespace Shlinkio\Shlink\CLI\GeoLite; use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException; diff --git a/module/CLI/test/Command/Visit/DownloadGeoLiteDbCommandTest.php b/module/CLI/test/Command/Visit/DownloadGeoLiteDbCommandTest.php index 62ea161a..28977ba8 100644 --- a/module/CLI/test/Command/Visit/DownloadGeoLiteDbCommandTest.php +++ b/module/CLI/test/Command/Visit/DownloadGeoLiteDbCommandTest.php @@ -9,8 +9,8 @@ use Prophecy\Argument; use Prophecy\Prophecy\ObjectProphecy; use Shlinkio\Shlink\CLI\Command\Visit\DownloadGeoLiteDbCommand; use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException; +use Shlinkio\Shlink\CLI\GeoLite\GeolocationDbUpdaterInterface; use Shlinkio\Shlink\CLI\Util\ExitCodes; -use Shlinkio\Shlink\CLI\Util\GeolocationDbUpdaterInterface; use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait; use Symfony\Component\Console\Tester\CommandTester; diff --git a/module/CLI/test/Util/GeolocationDbUpdaterTest.php b/module/CLI/test/GeoLite/GeolocationDbUpdaterTest.php similarity index 98% rename from module/CLI/test/Util/GeolocationDbUpdaterTest.php rename to module/CLI/test/GeoLite/GeolocationDbUpdaterTest.php index a884dd7c..9f09ee3b 100644 --- a/module/CLI/test/Util/GeolocationDbUpdaterTest.php +++ b/module/CLI/test/GeoLite/GeolocationDbUpdaterTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace ShlinkioTest\Shlink\CLI\Util; +namespace ShlinkioTest\Shlink\CLI\GeoLite; use Cake\Chronos\Chronos; use GeoIp2\Database\Reader; @@ -12,7 +12,7 @@ use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; use Prophecy\Prophecy\ObjectProphecy; use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException; -use Shlinkio\Shlink\CLI\Util\GeolocationDbUpdater; +use Shlinkio\Shlink\CLI\GeoLite\GeolocationDbUpdater; use Shlinkio\Shlink\Core\Options\TrackingOptions; use Shlinkio\Shlink\IpGeolocation\Exception\RuntimeException; use Shlinkio\Shlink\IpGeolocation\GeoLite2\DbUpdaterInterface; diff --git a/module/Core/config/event_dispatcher.config.php b/module/Core/config/event_dispatcher.config.php index 467f63cc..4b6c9048 100644 --- a/module/Core/config/event_dispatcher.config.php +++ b/module/Core/config/event_dispatcher.config.php @@ -6,7 +6,7 @@ namespace Shlinkio\Shlink\Core; use Laminas\ServiceManager\AbstractFactory\ConfigAbstractFactory; use Psr\EventDispatcher\EventDispatcherInterface; -use Shlinkio\Shlink\CLI\Util\GeolocationDbUpdater; +use Shlinkio\Shlink\CLI\GeoLite\GeolocationDbUpdater; use Shlinkio\Shlink\Common\Cache\RedisPublishingHelper; use Shlinkio\Shlink\Common\Mercure\MercureHubPublishingHelper; use Shlinkio\Shlink\Common\RabbitMq\RabbitMqPublishingHelper; diff --git a/module/Core/src/Action/AbstractTrackingAction.php b/module/Core/src/Action/AbstractTrackingAction.php index 8e9aaa09..0bf86258 100644 --- a/module/Core/src/Action/AbstractTrackingAction.php +++ b/module/Core/src/Action/AbstractTrackingAction.php @@ -19,8 +19,8 @@ use Shlinkio\Shlink\Core\Visit\RequestTrackerInterface; abstract class AbstractTrackingAction implements MiddlewareInterface, RequestMethodInterface { public function __construct( - private ShortUrlResolverInterface $urlResolver, - private RequestTrackerInterface $requestTracker, + private readonly ShortUrlResolverInterface $urlResolver, + private readonly RequestTrackerInterface $requestTracker, ) { } diff --git a/module/Core/src/EventDispatcher/UpdateGeoLiteDb.php b/module/Core/src/EventDispatcher/UpdateGeoLiteDb.php index 13941f43..b75b8a5d 100644 --- a/module/Core/src/EventDispatcher/UpdateGeoLiteDb.php +++ b/module/Core/src/EventDispatcher/UpdateGeoLiteDb.php @@ -5,7 +5,7 @@ declare(strict_types=1); namespace Shlinkio\Shlink\Core\EventDispatcher; use Psr\Log\LoggerInterface; -use Shlinkio\Shlink\CLI\Util\GeolocationDbUpdaterInterface; +use Shlinkio\Shlink\CLI\GeoLite\GeolocationDbUpdaterInterface; use Throwable; use function sprintf; diff --git a/module/Core/test/EventDispatcher/UpdateGeoLiteDbTest.php b/module/Core/test/EventDispatcher/UpdateGeoLiteDbTest.php index 178a142f..f9ba94fe 100644 --- a/module/Core/test/EventDispatcher/UpdateGeoLiteDbTest.php +++ b/module/Core/test/EventDispatcher/UpdateGeoLiteDbTest.php @@ -10,7 +10,7 @@ use Prophecy\PhpUnit\ProphecyTrait; use Prophecy\Prophecy\ObjectProphecy; use Psr\Log\LoggerInterface; use RuntimeException; -use Shlinkio\Shlink\CLI\Util\GeolocationDbUpdaterInterface; +use Shlinkio\Shlink\CLI\GeoLite\GeolocationDbUpdaterInterface; use Shlinkio\Shlink\Core\EventDispatcher\UpdateGeoLiteDb; class UpdateGeoLiteDbTest extends TestCase From eab9347522e77b4646f4f8c762132675f2258bbc Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 18 Sep 2022 10:31:14 +0200 Subject: [PATCH 100/108] Created enum to determine what was the result of updating a geolite DB --- module/CLI/src/GeoLite/GeolocationDbUpdater.php | 13 ++++++++----- .../src/GeoLite/GeolocationDbUpdaterInterface.php | 5 ++++- module/CLI/src/GeoLite/GeolocationResult.php | 11 +++++++++++ .../Command/Visit/DownloadGeoLiteDbCommandTest.php | 8 +++++--- .../CLI/test/GeoLite/GeolocationDbUpdaterTest.php | 9 ++++++--- .../test/EventDispatcher/UpdateGeoLiteDbTest.php | 9 +++++++-- 6 files changed, 41 insertions(+), 14 deletions(-) create mode 100644 module/CLI/src/GeoLite/GeolocationResult.php diff --git a/module/CLI/src/GeoLite/GeolocationDbUpdater.php b/module/CLI/src/GeoLite/GeolocationDbUpdater.php index 8061c1a0..b04ee3f2 100644 --- a/module/CLI/src/GeoLite/GeolocationDbUpdater.php +++ b/module/CLI/src/GeoLite/GeolocationDbUpdater.php @@ -30,17 +30,17 @@ class GeolocationDbUpdater implements GeolocationDbUpdaterInterface /** * @throws GeolocationDbUpdateFailedException */ - public function checkDbUpdate(?callable $beforeDownload = null, ?callable $handleProgress = null): void + public function checkDbUpdate(?callable $beforeDownload = null, ?callable $handleProgress = null): GeolocationResult { if ($this->trackingOptions->disableTracking || $this->trackingOptions->disableIpTracking) { - return; + return GeolocationResult::CHECK_SKIPPED; } $lock = $this->locker->createLock(self::LOCK_NAME); $lock->acquire(true); // Block until lock is released try { - $this->downloadIfNeeded($beforeDownload, $handleProgress); + return $this->downloadIfNeeded($beforeDownload, $handleProgress); } finally { $lock->release(); } @@ -49,17 +49,20 @@ class GeolocationDbUpdater implements GeolocationDbUpdaterInterface /** * @throws GeolocationDbUpdateFailedException */ - private function downloadIfNeeded(?callable $beforeDownload, ?callable $handleProgress): void + private function downloadIfNeeded(?callable $beforeDownload, ?callable $handleProgress): GeolocationResult { if (! $this->dbUpdater->databaseFileExists()) { $this->downloadNewDb(false, $beforeDownload, $handleProgress); - return; + return GeolocationResult::DB_CREATED; } $meta = $this->geoLiteDbReader->metadata(); if ($this->buildIsTooOld($meta)) { $this->downloadNewDb(true, $beforeDownload, $handleProgress); + return GeolocationResult::DB_UPDATED; } + + return GeolocationResult::DB_IS_UP_TO_DATE; } private function buildIsTooOld(Metadata $meta): bool diff --git a/module/CLI/src/GeoLite/GeolocationDbUpdaterInterface.php b/module/CLI/src/GeoLite/GeolocationDbUpdaterInterface.php index f7d5ecba..a143abb8 100644 --- a/module/CLI/src/GeoLite/GeolocationDbUpdaterInterface.php +++ b/module/CLI/src/GeoLite/GeolocationDbUpdaterInterface.php @@ -11,5 +11,8 @@ interface GeolocationDbUpdaterInterface /** * @throws GeolocationDbUpdateFailedException */ - public function checkDbUpdate(?callable $beforeDownload = null, ?callable $handleProgress = null): void; + public function checkDbUpdate( + ?callable $beforeDownload = null, + ?callable $handleProgress = null, + ): GeolocationResult; } diff --git a/module/CLI/src/GeoLite/GeolocationResult.php b/module/CLI/src/GeoLite/GeolocationResult.php new file mode 100644 index 00000000..7b245943 --- /dev/null +++ b/module/CLI/src/GeoLite/GeolocationResult.php @@ -0,0 +1,11 @@ + [function (): void { - }, '[INFO] GeoLite2 db file is up to date.']; - yield 'outdated db' => [function (array $args): void { + yield 'up to date db' => [fn () => GeolocationResult::CHECK_SKIPPED, '[INFO] GeoLite2 db file is up to date.']; + yield 'outdated db' => [function (array $args): GeolocationResult { [$beforeDownload] = $args; $beforeDownload(true); + + return GeolocationResult::DB_CREATED; }, '[OK] GeoLite2 db file properly downloaded.']; } } diff --git a/module/CLI/test/GeoLite/GeolocationDbUpdaterTest.php b/module/CLI/test/GeoLite/GeolocationDbUpdaterTest.php index 9f09ee3b..89667f3b 100644 --- a/module/CLI/test/GeoLite/GeolocationDbUpdaterTest.php +++ b/module/CLI/test/GeoLite/GeolocationDbUpdaterTest.php @@ -13,6 +13,7 @@ use Prophecy\PhpUnit\ProphecyTrait; use Prophecy\Prophecy\ObjectProphecy; use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException; use Shlinkio\Shlink\CLI\GeoLite\GeolocationDbUpdater; +use Shlinkio\Shlink\CLI\GeoLite\GeolocationResult; use Shlinkio\Shlink\Core\Options\TrackingOptions; use Shlinkio\Shlink\IpGeolocation\Exception\RuntimeException; use Shlinkio\Shlink\IpGeolocation\GeoLite2\DbUpdaterInterface; @@ -110,15 +111,16 @@ class GeolocationDbUpdaterTest extends TestCase * @test * @dataProvider provideSmallDays */ - public function databaseIsNotUpdatedIfItIsYoungerThanOneWeek(string|int $buildEpoch): void + public function databaseIsNotUpdatedIfItIsNewEnough(string|int $buildEpoch): void { $fileExists = $this->dbUpdater->databaseFileExists()->willReturn(true); $getMeta = $this->geoLiteDbReader->metadata()->willReturn($this->buildMetaWithBuildEpoch($buildEpoch)); $download = $this->dbUpdater->downloadFreshCopy(null)->will(function (): void { }); - $this->geolocationDbUpdater()->checkDbUpdate(); + $result = $this->geolocationDbUpdater()->checkDbUpdate(); + self::assertEquals(GeolocationResult::DB_IS_UP_TO_DATE, $result); $fileExists->shouldHaveBeenCalledOnce(); $getMeta->shouldHaveBeenCalledOnce(); $download->shouldNotHaveBeenCalled(); @@ -174,8 +176,9 @@ class GeolocationDbUpdaterTest extends TestCase */ public function downloadDbIsSkippedIfTrackingIsDisabled(TrackingOptions $options): void { - $this->geolocationDbUpdater($options)->checkDbUpdate(); + $result = $this->geolocationDbUpdater($options)->checkDbUpdate(); + self::assertEquals(GeolocationResult::CHECK_SKIPPED, $result); $this->dbUpdater->databaseFileExists(Argument::cetera())->shouldNotHaveBeenCalled(); $this->geoLiteDbReader->metadata(Argument::cetera())->shouldNotHaveBeenCalled(); } diff --git a/module/Core/test/EventDispatcher/UpdateGeoLiteDbTest.php b/module/Core/test/EventDispatcher/UpdateGeoLiteDbTest.php index f9ba94fe..a811fdc4 100644 --- a/module/Core/test/EventDispatcher/UpdateGeoLiteDbTest.php +++ b/module/Core/test/EventDispatcher/UpdateGeoLiteDbTest.php @@ -11,6 +11,7 @@ use Prophecy\Prophecy\ObjectProphecy; use Psr\Log\LoggerInterface; use RuntimeException; use Shlinkio\Shlink\CLI\GeoLite\GeolocationDbUpdaterInterface; +use Shlinkio\Shlink\CLI\GeoLite\GeolocationResult; use Shlinkio\Shlink\Core\EventDispatcher\UpdateGeoLiteDb; class UpdateGeoLiteDbTest extends TestCase @@ -51,9 +52,11 @@ class UpdateGeoLiteDbTest extends TestCase public function noticeMessageIsPrintedWhenFirstCallbackIsInvoked(bool $oldDbExists, string $expectedMessage): void { $checkDbUpdate = $this->dbUpdater->checkDbUpdate(Argument::cetera())->will( - function (array $args) use ($oldDbExists): void { + function (array $args) use ($oldDbExists): GeolocationResult { [$firstCallback] = $args; $firstCallback($oldDbExists); + + return GeolocationResult::DB_CREATED; }, ); $logNotice = $this->logger->notice($expectedMessage); @@ -82,13 +85,15 @@ class UpdateGeoLiteDbTest extends TestCase ?string $expectedMessage, ): void { $checkDbUpdate = $this->dbUpdater->checkDbUpdate(Argument::cetera())->will( - function (array $args) use ($total, $downloaded, $oldDbExists): void { + function (array $args) use ($total, $downloaded, $oldDbExists): GeolocationResult { [, $secondCallback] = $args; // Invoke several times to ensure the log is printed only once $secondCallback($total, $downloaded, $oldDbExists); $secondCallback($total, $downloaded, $oldDbExists); $secondCallback($total, $downloaded, $oldDbExists); + + return GeolocationResult::DB_CREATED; }, ); $logNotice = $this->logger->notice($expectedMessage ?? Argument::cetera()); From ef01754ad5f0e7e703b75343e635412ebf64ba05 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 18 Sep 2022 11:17:17 +0200 Subject: [PATCH 101/108] Added event dispatching to UpdateGeoLiteDb dispatcher so that it locates visits when file has just been created --- ...l-geolite2-db-download-in-docker-images.md | 0 docs/adr/README.md | 1 + .../Core/config/event_dispatcher.config.php | 9 +++- .../Event/GeoLiteDbCreated.php | 9 ++++ .../src/EventDispatcher/UpdateGeoLiteDb.php | 15 +++++-- .../EventDispatcher/UpdateGeoLiteDbTest.php | 43 +++++++++++++++++-- 6 files changed, 70 insertions(+), 7 deletions(-) create mode 100644 docs/adr/2022-09-18-allow-delaying-initial-geolite2-db-download-in-docker-images.md create mode 100644 module/Core/src/EventDispatcher/Event/GeoLiteDbCreated.php diff --git a/docs/adr/2022-09-18-allow-delaying-initial-geolite2-db-download-in-docker-images.md b/docs/adr/2022-09-18-allow-delaying-initial-geolite2-db-download-in-docker-images.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/adr/README.md b/docs/adr/README.md index 7cfccdf7..f12d17c1 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -2,6 +2,7 @@ Here listed you will find the different architectural decisions taken in the project, including all the reasoning behind it, options considered, and final outcome. +* [2022-09-18 Allow delaying initial GeoLite2 DB download in docker images](2022-09-18-allow-delaying-initial-geolite2-db-download-in-docker-images.md) * [2022-08-05 Support multi-segment custom slugs](2022-08-05-support-multi-segment-custom-slugs.md) * [2022-01-15 Update env vars behavior to have precedence over installer options](2022-01-15-update-env-vars-behavior-to-have-precedence-over-installer-options.md) * [2021-08-05 Migrate to a new caching library](2021-08-05-migrate-to-a-new-caching-library.md) diff --git a/module/Core/config/event_dispatcher.config.php b/module/Core/config/event_dispatcher.config.php index 4b6c9048..3a4d10f5 100644 --- a/module/Core/config/event_dispatcher.config.php +++ b/module/Core/config/event_dispatcher.config.php @@ -20,6 +20,9 @@ return [ EventDispatcher\Event\UrlVisited::class => [ EventDispatcher\LocateVisit::class, ], + EventDispatcher\Event\GeoLiteDbCreated::class => [ +// EventDispatcher\LocateUnloctedVisits::class, + ], ], 'async' => [ EventDispatcher\Event\VisitLocated::class => [ @@ -132,7 +135,11 @@ return [ 'Logger_Shlink', 'config.redis.pub_sub_enabled', ], - EventDispatcher\UpdateGeoLiteDb::class => [GeolocationDbUpdater::class, 'Logger_Shlink'], + EventDispatcher\UpdateGeoLiteDb::class => [ + GeolocationDbUpdater::class, + 'Logger_Shlink', + EventDispatcherInterface::class, + ], ], ]; diff --git a/module/Core/src/EventDispatcher/Event/GeoLiteDbCreated.php b/module/Core/src/EventDispatcher/Event/GeoLiteDbCreated.php new file mode 100644 index 00000000..3fc86cd7 --- /dev/null +++ b/module/Core/src/EventDispatcher/Event/GeoLiteDbCreated.php @@ -0,0 +1,9 @@ +dbUpdater->checkDbUpdate($beforeDownload, $handleProgress); + $result = $this->dbUpdater->checkDbUpdate($beforeDownload, $handleProgress); + if ($result === GeolocationResult::DB_CREATED) { + $this->eventDispatcher->dispatch(new GeoLiteDbCreated()); + } } catch (Throwable $e) { $this->logger->error('GeoLite2 database download failed. {e}', ['e' => $e]); } diff --git a/module/Core/test/EventDispatcher/UpdateGeoLiteDbTest.php b/module/Core/test/EventDispatcher/UpdateGeoLiteDbTest.php index a811fdc4..9ce20801 100644 --- a/module/Core/test/EventDispatcher/UpdateGeoLiteDbTest.php +++ b/module/Core/test/EventDispatcher/UpdateGeoLiteDbTest.php @@ -8,12 +8,16 @@ use PHPUnit\Framework\TestCase; use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; use Prophecy\Prophecy\ObjectProphecy; +use Psr\EventDispatcher\EventDispatcherInterface; use Psr\Log\LoggerInterface; use RuntimeException; use Shlinkio\Shlink\CLI\GeoLite\GeolocationDbUpdaterInterface; use Shlinkio\Shlink\CLI\GeoLite\GeolocationResult; +use Shlinkio\Shlink\Core\EventDispatcher\Event\GeoLiteDbCreated; use Shlinkio\Shlink\Core\EventDispatcher\UpdateGeoLiteDb; +use function Functional\map; + class UpdateGeoLiteDbTest extends TestCase { use ProphecyTrait; @@ -21,13 +25,19 @@ class UpdateGeoLiteDbTest extends TestCase private UpdateGeoLiteDb $listener; private ObjectProphecy $dbUpdater; private ObjectProphecy $logger; + private ObjectProphecy $eventDispatcher; protected function setUp(): void { $this->dbUpdater = $this->prophesize(GeolocationDbUpdaterInterface::class); $this->logger = $this->prophesize(LoggerInterface::class); + $this->eventDispatcher = $this->prophesize(EventDispatcherInterface::class); - $this->listener = new UpdateGeoLiteDb($this->dbUpdater->reveal(), $this->logger->reveal()); + $this->listener = new UpdateGeoLiteDb( + $this->dbUpdater->reveal(), + $this->logger->reveal(), + $this->eventDispatcher->reveal(), + ); } /** @test */ @@ -43,6 +53,7 @@ class UpdateGeoLiteDbTest extends TestCase $checkDbUpdate->shouldHaveBeenCalledOnce(); $logError->shouldHaveBeenCalledOnce(); $this->logger->notice(Argument::cetera())->shouldNotHaveBeenCalled(); + $this->eventDispatcher->dispatch(Argument::cetera())->shouldNotHaveBeenCalled(); } /** @@ -56,7 +67,7 @@ class UpdateGeoLiteDbTest extends TestCase [$firstCallback] = $args; $firstCallback($oldDbExists); - return GeolocationResult::DB_CREATED; + return GeolocationResult::DB_IS_UP_TO_DATE; }, ); $logNotice = $this->logger->notice($expectedMessage); @@ -66,6 +77,7 @@ class UpdateGeoLiteDbTest extends TestCase $checkDbUpdate->shouldHaveBeenCalledOnce(); $logNotice->shouldHaveBeenCalledOnce(); $this->logger->error(Argument::cetera())->shouldNotHaveBeenCalled(); + $this->eventDispatcher->dispatch(Argument::cetera())->shouldNotHaveBeenCalled(); } public function provideFlags(): iterable @@ -93,7 +105,7 @@ class UpdateGeoLiteDbTest extends TestCase $secondCallback($total, $downloaded, $oldDbExists); $secondCallback($total, $downloaded, $oldDbExists); - return GeolocationResult::DB_CREATED; + return GeolocationResult::DB_UPDATED; }, ); $logNotice = $this->logger->notice($expectedMessage ?? Argument::cetera()); @@ -107,6 +119,7 @@ class UpdateGeoLiteDbTest extends TestCase } $checkDbUpdate->shouldHaveBeenCalledOnce(); $this->logger->error(Argument::cetera())->shouldNotHaveBeenCalled(); + $this->eventDispatcher->dispatch(Argument::cetera())->shouldNotHaveBeenCalled(); } public function provideDownloaded(): iterable @@ -120,4 +133,28 @@ class UpdateGeoLiteDbTest extends TestCase yield [100, 101, true, 'Finished updating GeoLite2 db file']; yield [100, 101, false, 'Finished downloading GeoLite2 db file']; } + + /** + * @test + * @dataProvider provideGeolocationResults + */ + public function dispatchesEventOnlyWhenDbFileHasBeenCreatedForTheFirstTime( + GeolocationResult $result, + int $expectedDispatches, + ): void { + $checkDbUpdate = $this->dbUpdater->checkDbUpdate(Argument::cetera())->willReturn($result); + + ($this->listener)(); + + $checkDbUpdate->shouldHaveBeenCalledOnce(); + $this->eventDispatcher->dispatch(new GeoLiteDbCreated())->shouldHaveBeenCalledTimes($expectedDispatches); + } + + public function provideGeolocationResults(): iterable + { + return map(GeolocationResult::cases(), static fn (GeolocationResult $value) => [ + $value, + $value === GeolocationResult::DB_CREATED ? 1 : 0, + ]); + } } From 6f17f70137d1da868a6d3a15be5fa6dd6ba6a9c1 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 18 Sep 2022 17:00:03 +0200 Subject: [PATCH 102/108] Allowed to delay GeoLite2 db download on docker images --- composer.json | 2 +- docker/docker-entrypoint.sh | 6 +- module/CLI/config/dependencies.config.php | 2 + .../src/Command/Visit/LocateVisitsCommand.php | 4 +- .../CLI/src/GeoLite/GeolocationDbUpdater.php | 33 ++++++----- .../test/GeoLite/GeolocationDbUpdaterTest.php | 6 +- .../Core/config/event_dispatcher.config.php | 8 ++- .../EventDispatcher/LocateUnlocatedVisits.php | 57 +++++++++++++++++++ 8 files changed, 95 insertions(+), 23 deletions(-) create mode 100644 module/Core/src/EventDispatcher/LocateUnlocatedVisits.php diff --git a/composer.json b/composer.json index d6122753..c99678ed 100644 --- a/composer.json +++ b/composer.json @@ -50,7 +50,7 @@ "shlinkio/shlink-event-dispatcher": "dev-main#48c0137 as 2.6", "shlinkio/shlink-importer": "^4.0", "shlinkio/shlink-installer": "dev-develop#a01bca9 as 8.2", - "shlinkio/shlink-ip-geolocation": "^3.0", + "shlinkio/shlink-ip-geolocation": "^3.1", "spiral/roadrunner": "^2.11", "spiral/roadrunner-jobs": "^2.3", "symfony/console": "^6.1", diff --git a/docker/docker-entrypoint.sh b/docker/docker-entrypoint.sh index f28627d2..fb8b7bf2 100644 --- a/docker/docker-entrypoint.sh +++ b/docker/docker-entrypoint.sh @@ -18,14 +18,14 @@ php bin/doctrine orm:generate-proxies -n ${flags} echo "Clearing entities cache..." php bin/doctrine orm:clear-cache:metadata -n ${flags} -# Try to download GeoLite2 db file only if the license key env var was defined -if [ ! -z "${GEOLITE_LICENSE_KEY}" ]; then +# Try to download GeoLite2 db file only if the license key env var was defined and skipping was not explicitly set +if [ ! -z "${GEOLITE_LICENSE_KEY}" ] && [ "${SKIP_INITIAL_GEOLITE_DOWNLOAD}" != "true" ]; then echo "Downloading GeoLite2 db file..." php bin/cli visit:download-db -n ${flags} fi # Periodically run visit:locate every hour, if ENABLE_PERIODIC_VISIT_LOCATE=true was provided -if [ $ENABLE_PERIODIC_VISIT_LOCATE ]; then +if [ "${ENABLE_PERIODIC_VISIT_LOCATE}" = "true" ]; then echo "Configuring periodic visit location..." echo "0 * * * * php /etc/shlink/bin/cli visit:locate -q" > /etc/crontabs/root /usr/sbin/crond & diff --git a/module/CLI/config/dependencies.config.php b/module/CLI/config/dependencies.config.php index 38368746..a9de1f0c 100644 --- a/module/CLI/config/dependencies.config.php +++ b/module/CLI/config/dependencies.config.php @@ -19,6 +19,7 @@ use Shlinkio\Shlink\Core\Tag\TagService; use Shlinkio\Shlink\Core\Visit; use Shlinkio\Shlink\Installer\Factory\ProcessHelperFactory; use Shlinkio\Shlink\IpGeolocation\GeoLite2\DbUpdater; +use Shlinkio\Shlink\IpGeolocation\GeoLite2\GeoLite2Options; use Shlinkio\Shlink\IpGeolocation\Resolver\IpLocationResolverInterface; use Shlinkio\Shlink\Rest\Service\ApiKeyService; use Symfony\Component\Console as SymfonyCli; @@ -75,6 +76,7 @@ return [ Reader::class, LOCAL_LOCK_FACTORY, TrackingOptions::class, + GeoLite2Options::class, ], Util\ProcessRunner::class => [SymfonyCli\Helper\ProcessHelper::class], ApiKey\RoleResolver::class => [DomainService::class, 'config.url_shortener.domain.hostname'], diff --git a/module/CLI/src/Command/Visit/LocateVisitsCommand.php b/module/CLI/src/Command/Visit/LocateVisitsCommand.php index fe898dbb..47aa50d1 100644 --- a/module/CLI/src/Command/Visit/LocateVisitsCommand.php +++ b/module/CLI/src/Command/Visit/LocateVisitsCommand.php @@ -34,8 +34,8 @@ class LocateVisitsCommand extends AbstractLockedCommand implements VisitGeolocat private SymfonyStyle $io; public function __construct( - private VisitLocatorInterface $visitLocator, - private IpLocationResolverInterface $ipLocationResolver, + private readonly VisitLocatorInterface $visitLocator, + private readonly IpLocationResolverInterface $ipLocationResolver, LockFactory $locker, ) { parent::__construct($locker); diff --git a/module/CLI/src/GeoLite/GeolocationDbUpdater.php b/module/CLI/src/GeoLite/GeolocationDbUpdater.php index b04ee3f2..f33b8796 100644 --- a/module/CLI/src/GeoLite/GeolocationDbUpdater.php +++ b/module/CLI/src/GeoLite/GeolocationDbUpdater.php @@ -9,7 +9,9 @@ use GeoIp2\Database\Reader; use MaxMind\Db\Reader\Metadata; use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException; use Shlinkio\Shlink\Core\Options\TrackingOptions; -use Shlinkio\Shlink\IpGeolocation\Exception\RuntimeException; +use Shlinkio\Shlink\IpGeolocation\Exception\DbUpdateException; +use Shlinkio\Shlink\IpGeolocation\Exception\MissingLicenseException; +use Shlinkio\Shlink\IpGeolocation\Exception\WrongIpException; use Shlinkio\Shlink\IpGeolocation\GeoLite2\DbUpdaterInterface; use Symfony\Component\Lock\LockFactory; @@ -20,10 +22,10 @@ class GeolocationDbUpdater implements GeolocationDbUpdaterInterface private const LOCK_NAME = 'geolocation-db-update'; public function __construct( - private DbUpdaterInterface $dbUpdater, - private Reader $geoLiteDbReader, - private LockFactory $locker, - private TrackingOptions $trackingOptions, + private readonly DbUpdaterInterface $dbUpdater, + private readonly Reader $geoLiteDbReader, + private readonly LockFactory $locker, + private readonly TrackingOptions $trackingOptions, ) { } @@ -52,14 +54,12 @@ class GeolocationDbUpdater implements GeolocationDbUpdaterInterface private function downloadIfNeeded(?callable $beforeDownload, ?callable $handleProgress): GeolocationResult { if (! $this->dbUpdater->databaseFileExists()) { - $this->downloadNewDb(false, $beforeDownload, $handleProgress); - return GeolocationResult::DB_CREATED; + return $this->downloadNewDb(false, $beforeDownload, $handleProgress); } $meta = $this->geoLiteDbReader->metadata(); if ($this->buildIsTooOld($meta)) { - $this->downloadNewDb(true, $beforeDownload, $handleProgress); - return GeolocationResult::DB_UPDATED; + return $this->downloadNewDb(true, $beforeDownload, $handleProgress); } return GeolocationResult::DB_IS_UP_TO_DATE; @@ -95,15 +95,22 @@ class GeolocationDbUpdater implements GeolocationDbUpdaterInterface /** * @throws GeolocationDbUpdateFailedException */ - private function downloadNewDb(bool $olderDbExists, ?callable $beforeDownload, ?callable $handleProgress): void - { + private function downloadNewDb( + bool $olderDbExists, + ?callable $beforeDownload, + ?callable $handleProgress, + ): GeolocationResult { if ($beforeDownload !== null) { $beforeDownload($olderDbExists); } try { $this->dbUpdater->downloadFreshCopy($this->wrapHandleProgressCallback($handleProgress, $olderDbExists)); - } catch (RuntimeException $e) { + return $olderDbExists ? GeolocationResult::DB_UPDATED : GeolocationResult::DB_CREATED; + } catch (MissingLicenseException) { + // If there's no license key, just ignore the error + return GeolocationResult::CHECK_SKIPPED; + } catch (DbUpdateException | WrongIpException $e) { throw $olderDbExists ? GeolocationDbUpdateFailedException::withOlderDb($e) : GeolocationDbUpdateFailedException::withoutOlderDb($e); @@ -116,6 +123,6 @@ class GeolocationDbUpdater implements GeolocationDbUpdaterInterface return null; } - return fn (int $total, int $downloaded) => $handleProgress($total, $downloaded, $olderDbExists); + return static fn (int $total, int $downloaded) => $handleProgress($total, $downloaded, $olderDbExists); } } diff --git a/module/CLI/test/GeoLite/GeolocationDbUpdaterTest.php b/module/CLI/test/GeoLite/GeolocationDbUpdaterTest.php index 89667f3b..61056922 100644 --- a/module/CLI/test/GeoLite/GeolocationDbUpdaterTest.php +++ b/module/CLI/test/GeoLite/GeolocationDbUpdaterTest.php @@ -15,7 +15,7 @@ use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException; use Shlinkio\Shlink\CLI\GeoLite\GeolocationDbUpdater; use Shlinkio\Shlink\CLI\GeoLite\GeolocationResult; use Shlinkio\Shlink\Core\Options\TrackingOptions; -use Shlinkio\Shlink\IpGeolocation\Exception\RuntimeException; +use Shlinkio\Shlink\IpGeolocation\Exception\DbUpdateException; use Shlinkio\Shlink\IpGeolocation\GeoLite2\DbUpdaterInterface; use Symfony\Component\Lock; use Throwable; @@ -48,7 +48,7 @@ class GeolocationDbUpdaterTest extends TestCase public function exceptionIsThrownWhenOlderDbDoesNotExistAndDownloadFails(): void { $mustBeUpdated = fn () => self::assertTrue(true); - $prev = new RuntimeException(''); + $prev = new DbUpdateException(''); $fileExists = $this->dbUpdater->databaseFileExists()->willReturn(false); $getMeta = $this->geoLiteDbReader->metadata(); @@ -81,7 +81,7 @@ class GeolocationDbUpdaterTest extends TestCase $getMeta = $this->geoLiteDbReader->metadata()->willReturn($this->buildMetaWithBuildEpoch( Chronos::now()->subDays($days)->getTimestamp(), )); - $prev = new RuntimeException(''); + $prev = new DbUpdateException(''); $download = $this->dbUpdater->downloadFreshCopy(null)->willThrow($prev); try { diff --git a/module/Core/config/event_dispatcher.config.php b/module/Core/config/event_dispatcher.config.php index 3a4d10f5..f6e198b8 100644 --- a/module/Core/config/event_dispatcher.config.php +++ b/module/Core/config/event_dispatcher.config.php @@ -10,6 +10,7 @@ use Shlinkio\Shlink\CLI\GeoLite\GeolocationDbUpdater; use Shlinkio\Shlink\Common\Cache\RedisPublishingHelper; use Shlinkio\Shlink\Common\Mercure\MercureHubPublishingHelper; use Shlinkio\Shlink\Common\RabbitMq\RabbitMqPublishingHelper; +use Shlinkio\Shlink\Core\Visit\VisitLocator; use Shlinkio\Shlink\IpGeolocation\GeoLite2\DbUpdater; use Shlinkio\Shlink\IpGeolocation\Resolver\IpLocationResolverInterface; @@ -21,7 +22,7 @@ return [ EventDispatcher\LocateVisit::class, ], EventDispatcher\Event\GeoLiteDbCreated::class => [ -// EventDispatcher\LocateUnloctedVisits::class, + EventDispatcher\LocateUnlocatedVisits::class, ], ], 'async' => [ @@ -43,6 +44,7 @@ return [ 'dependencies' => [ 'factories' => [ EventDispatcher\LocateVisit::class => ConfigAbstractFactory::class, + EventDispatcher\LocateUnlocatedVisits::class => ConfigAbstractFactory::class, EventDispatcher\NotifyVisitToWebHooks::class => ConfigAbstractFactory::class, EventDispatcher\Mercure\NotifyVisitToMercure::class => ConfigAbstractFactory::class, EventDispatcher\Mercure\NotifyNewShortUrlToMercure::class => ConfigAbstractFactory::class, @@ -72,6 +74,9 @@ return [ EventDispatcher\RedisPubSub\NotifyNewShortUrlToRedis::class => [ EventDispatcher\CloseDbConnectionEventListenerDelegator::class, ], + EventDispatcher\LocateUnlocatedVisits::class => [ + EventDispatcher\CloseDbConnectionEventListenerDelegator::class, + ], EventDispatcher\NotifyVisitToWebHooks::class => [ EventDispatcher\CloseDbConnectionEventListenerDelegator::class, ], @@ -86,6 +91,7 @@ return [ DbUpdater::class, EventDispatcherInterface::class, ], + EventDispatcher\LocateUnlocatedVisits::class => [VisitLocator::class, IpLocationResolverInterface::class], EventDispatcher\NotifyVisitToWebHooks::class => [ 'httpClient', 'em', diff --git a/module/Core/src/EventDispatcher/LocateUnlocatedVisits.php b/module/Core/src/EventDispatcher/LocateUnlocatedVisits.php new file mode 100644 index 00000000..a72e8853 --- /dev/null +++ b/module/Core/src/EventDispatcher/LocateUnlocatedVisits.php @@ -0,0 +1,57 @@ +locator->locateUnlocatedVisits($this); + } + + /** + * @throws IpCannotBeLocatedException + */ + public function geolocateVisit(Visit $visit): Location + { + // TODO This method duplicates code from LocateVisitsCommand. Move to a common place. + if (! $visit->hasRemoteAddr()) { + throw IpCannotBeLocatedException::forEmptyAddress(); + } + + $ipAddr = $visit->getRemoteAddr() ?? ''; + if ($ipAddr === IpAddress::LOCALHOST) { + throw IpCannotBeLocatedException::forLocalhost(); + } + + try { + return $this->ipLocationResolver->resolveIpLocation($ipAddr); + } catch (WrongIpException $e) { + throw IpCannotBeLocatedException::forError($e); + } + } + + public function onVisitLocated(VisitLocation $visitLocation, Visit $visit): void + { + // Do nothing + } +} From d76e6647d29714881aa336dd11fa8fdfc7b1d2c9 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 18 Sep 2022 17:10:11 +0200 Subject: [PATCH 103/108] Added real version for composer dependencies --- composer.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/composer.json b/composer.json index c99678ed..8be0e37a 100644 --- a/composer.json +++ b/composer.json @@ -45,11 +45,11 @@ "php-middleware/request-id": "^4.1", "pugx/shortid-php": "^1.0", "ramsey/uuid": "^4.3", - "shlinkio/shlink-common": "dev-main#c9e6474 as 5.1", - "shlinkio/shlink-config": "dev-main#12fb295 as 2.1", - "shlinkio/shlink-event-dispatcher": "dev-main#48c0137 as 2.6", + "shlinkio/shlink-common": "^5.1", + "shlinkio/shlink-config": "^2.1", + "shlinkio/shlink-event-dispatcher": "^2.6", "shlinkio/shlink-importer": "^4.0", - "shlinkio/shlink-installer": "dev-develop#a01bca9 as 8.2", + "shlinkio/shlink-installer": "^8.2", "shlinkio/shlink-ip-geolocation": "^3.1", "spiral/roadrunner": "^2.11", "spiral/roadrunner-jobs": "^2.3", @@ -73,7 +73,7 @@ "phpunit/phpunit": "^9.5", "roave/security-advisories": "dev-master", "shlinkio/php-coding-standard": "~2.3.0", - "shlinkio/shlink-test-utils": "dev-main#404fdf6 as 3.3", + "shlinkio/shlink-test-utils": "^3.3", "symfony/var-dumper": "^6.1", "veewee/composer-run-parallel": "^1.1" }, From fe41e9d573a7c7338148047f4104d72cbbd7a001 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 18 Sep 2022 17:12:21 +0200 Subject: [PATCH 104/108] Updated changelog --- CHANGELOG.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f85af37f..d491db0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ 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] +## [3.3.0] - 2022-09-18 ### Added * [#1221](https://github.com/shlinkio/shlink/issues/1221) Added experimental support to run Shlink with [RoadRunner](https://roadrunner.dev) instead of openswoole. * [#1531](https://github.com/shlinkio/shlink/issues/1531) and [#1090](https://github.com/shlinkio/shlink/issues/1090) Added support for trailing slashes in short URLs. @@ -34,6 +34,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this Also, the installer tool now allows to generate an initial API key that can be copy-pasted (this tool is run interactively), in case you use php-fpm or you don't want to use env vars. +* [#1528](https://github.com/shlinkio/shlink/issues/1528) Added support to delay when the GeoLite2 DB file is downloaded in docker images, speeding up its startup time. + + In order to do it, pass `SKIP_INITIAL_GEOLITE_DOWNLOAD=true` when creating the container. + ### Changed * [#1339](https://github.com/shlinkio/shlink/issues/1339) Added new test suite for CLI E2E tests. * [#1503](https://github.com/shlinkio/shlink/issues/1503) Drastically improved build time in GitHub Actions, by optimizing parallelization and adding php extensions cache. From 83b7d5a5f1734afcd917fadbee23d7d8882b28d9 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 18 Sep 2022 18:44:01 +0200 Subject: [PATCH 105/108] Extracted logic to geolocate a visit, handling possible domain errors --- module/Core/config/dependencies.config.php | 3 + .../Core/config/event_dispatcher.config.php | 3 +- .../EventDispatcher/LocateUnlocatedVisits.php | 23 +------ .../Exception/IpCannotBeLocatedException.php | 23 ++++--- .../src/Visit/Model/UnlocatableIpType.php | 10 +++ .../Core/src/Visit/VisitToLocationHelper.php | 40 +++++++++++ .../Visit/VisitToLocationHelperInterface.php | 17 +++++ .../LocateUnlocatedVisitsTest.php | 54 +++++++++++++++ .../IpCannotBeLocatedExceptionTest.php | 4 ++ module/Core/test/Visit/VisitLocatorTest.php | 2 +- .../test/Visit/VisitToLocationHelperTest.php | 66 +++++++++++++++++++ 11 files changed, 214 insertions(+), 31 deletions(-) create mode 100644 module/Core/src/Visit/Model/UnlocatableIpType.php create mode 100644 module/Core/src/Visit/VisitToLocationHelper.php create mode 100644 module/Core/src/Visit/VisitToLocationHelperInterface.php create mode 100644 module/Core/test/EventDispatcher/LocateUnlocatedVisitsTest.php create mode 100644 module/Core/test/Visit/VisitToLocationHelperTest.php diff --git a/module/Core/config/dependencies.config.php b/module/Core/config/dependencies.config.php index 9855e2aa..49b2857a 100644 --- a/module/Core/config/dependencies.config.php +++ b/module/Core/config/dependencies.config.php @@ -11,6 +11,7 @@ use Shlinkio\Shlink\Config\Factory\ValinorConfigFactory; use Shlinkio\Shlink\Core\ErrorHandler; use Shlinkio\Shlink\Core\Options\NotFoundRedirectOptions; use Shlinkio\Shlink\Importer\ImportedLinksProcessorInterface; +use Shlinkio\Shlink\IpGeolocation\Resolver\IpLocationResolverInterface; return [ @@ -44,6 +45,7 @@ return [ Visit\VisitsTracker::class => ConfigAbstractFactory::class, Visit\RequestTracker::class => ConfigAbstractFactory::class, Visit\VisitLocator::class => ConfigAbstractFactory::class, + Visit\VisitToLocationHelper::class => ConfigAbstractFactory::class, Visit\VisitsStatsHelper::class => ConfigAbstractFactory::class, Visit\Transformer\OrphanVisitDataTransformer::class => InvokableFactory::class, @@ -108,6 +110,7 @@ return [ ShortUrl\Resolver\PersistenceShortUrlRelationResolver::class, ], Visit\VisitLocator::class => ['em'], + Visit\VisitToLocationHelper::class => [IpLocationResolverInterface::class], Visit\VisitsStatsHelper::class => ['em'], Tag\TagService::class => ['em'], Service\ShortUrl\DeleteShortUrlService::class => [ diff --git a/module/Core/config/event_dispatcher.config.php b/module/Core/config/event_dispatcher.config.php index f6e198b8..3d473010 100644 --- a/module/Core/config/event_dispatcher.config.php +++ b/module/Core/config/event_dispatcher.config.php @@ -11,6 +11,7 @@ use Shlinkio\Shlink\Common\Cache\RedisPublishingHelper; use Shlinkio\Shlink\Common\Mercure\MercureHubPublishingHelper; use Shlinkio\Shlink\Common\RabbitMq\RabbitMqPublishingHelper; use Shlinkio\Shlink\Core\Visit\VisitLocator; +use Shlinkio\Shlink\Core\Visit\VisitToLocationHelper; use Shlinkio\Shlink\IpGeolocation\GeoLite2\DbUpdater; use Shlinkio\Shlink\IpGeolocation\Resolver\IpLocationResolverInterface; @@ -91,7 +92,7 @@ return [ DbUpdater::class, EventDispatcherInterface::class, ], - EventDispatcher\LocateUnlocatedVisits::class => [VisitLocator::class, IpLocationResolverInterface::class], + EventDispatcher\LocateUnlocatedVisits::class => [VisitLocator::class, VisitToLocationHelper::class], EventDispatcher\NotifyVisitToWebHooks::class => [ 'httpClient', 'em', diff --git a/module/Core/src/EventDispatcher/LocateUnlocatedVisits.php b/module/Core/src/EventDispatcher/LocateUnlocatedVisits.php index a72e8853..c036c450 100644 --- a/module/Core/src/EventDispatcher/LocateUnlocatedVisits.php +++ b/module/Core/src/EventDispatcher/LocateUnlocatedVisits.php @@ -4,22 +4,20 @@ declare(strict_types=1); namespace Shlinkio\Shlink\Core\EventDispatcher; -use Shlinkio\Shlink\Common\Util\IpAddress; use Shlinkio\Shlink\Core\Entity\Visit; use Shlinkio\Shlink\Core\Entity\VisitLocation; use Shlinkio\Shlink\Core\EventDispatcher\Event\GeoLiteDbCreated; use Shlinkio\Shlink\Core\Exception\IpCannotBeLocatedException; use Shlinkio\Shlink\Core\Visit\VisitGeolocationHelperInterface; use Shlinkio\Shlink\Core\Visit\VisitLocatorInterface; -use Shlinkio\Shlink\IpGeolocation\Exception\WrongIpException; +use Shlinkio\Shlink\Core\Visit\VisitToLocationHelperInterface; use Shlinkio\Shlink\IpGeolocation\Model\Location; -use Shlinkio\Shlink\IpGeolocation\Resolver\IpLocationResolverInterface; class LocateUnlocatedVisits implements VisitGeolocationHelperInterface { public function __construct( private readonly VisitLocatorInterface $locator, - private readonly IpLocationResolverInterface $ipLocationResolver, + private readonly VisitToLocationHelperInterface $visitToLocation, ) { } @@ -33,25 +31,10 @@ class LocateUnlocatedVisits implements VisitGeolocationHelperInterface */ public function geolocateVisit(Visit $visit): Location { - // TODO This method duplicates code from LocateVisitsCommand. Move to a common place. - if (! $visit->hasRemoteAddr()) { - throw IpCannotBeLocatedException::forEmptyAddress(); - } - - $ipAddr = $visit->getRemoteAddr() ?? ''; - if ($ipAddr === IpAddress::LOCALHOST) { - throw IpCannotBeLocatedException::forLocalhost(); - } - - try { - return $this->ipLocationResolver->resolveIpLocation($ipAddr); - } catch (WrongIpException $e) { - throw IpCannotBeLocatedException::forError($e); - } + return $this->visitToLocation->resolveVisitLocation($visit); } public function onVisitLocated(VisitLocation $visitLocation, Visit $visit): void { - // Do nothing } } diff --git a/module/Core/src/Exception/IpCannotBeLocatedException.php b/module/Core/src/Exception/IpCannotBeLocatedException.php index b1ba731c..2ebc3e62 100644 --- a/module/Core/src/Exception/IpCannotBeLocatedException.php +++ b/module/Core/src/Exception/IpCannotBeLocatedException.php @@ -4,35 +4,40 @@ declare(strict_types=1); namespace Shlinkio\Shlink\Core\Exception; +use Shlinkio\Shlink\Core\Visit\Model\UnlocatableIpType; use Throwable; class IpCannotBeLocatedException extends RuntimeException { - private bool $isNonLocatableAddress = true; + private function __construct( + string $message, + public readonly UnlocatableIpType $type, + int $code = 0, + ?Throwable $previous = null, + ) { + parent::__construct($message, $code, $previous); + } public static function forEmptyAddress(): self { - return new self('Ignored visit with no IP address'); + return new self('Ignored visit with no IP address', UnlocatableIpType::EMPTY_ADDRESS); } public static function forLocalhost(): self { - return new self('Ignored localhost address'); + return new self('Ignored localhost address', UnlocatableIpType::LOCALHOST); } public static function forError(Throwable $e): self { - $e = new self('An error occurred while locating IP', $e->getCode(), $e); - $e->isNonLocatableAddress = false; - - return $e; + return new self('An error occurred while locating IP', UnlocatableIpType::ERROR, $e->getCode(), $e); } /** - * Tells if this error belongs to an address that will never be possible locate + * Tells if this belongs to an address that will never be possible to locate */ public function isNonLocatableAddress(): bool { - return $this->isNonLocatableAddress; + return $this->type !== UnlocatableIpType::ERROR; } } diff --git a/module/Core/src/Visit/Model/UnlocatableIpType.php b/module/Core/src/Visit/Model/UnlocatableIpType.php new file mode 100644 index 00000000..56490209 --- /dev/null +++ b/module/Core/src/Visit/Model/UnlocatableIpType.php @@ -0,0 +1,10 @@ +hasRemoteAddr()) { + throw IpCannotBeLocatedException::forEmptyAddress(); + } + + $ipAddr = $visit->getRemoteAddr() ?? ''; + if ($ipAddr === IpAddress::LOCALHOST) { + throw IpCannotBeLocatedException::forLocalhost(); + } + + try { + return $this->ipLocationResolver->resolveIpLocation($ipAddr); + } catch (WrongIpException $e) { + throw IpCannotBeLocatedException::forError($e); + } + } +} diff --git a/module/Core/src/Visit/VisitToLocationHelperInterface.php b/module/Core/src/Visit/VisitToLocationHelperInterface.php new file mode 100644 index 00000000..7d553527 --- /dev/null +++ b/module/Core/src/Visit/VisitToLocationHelperInterface.php @@ -0,0 +1,17 @@ +locator = $this->prophesize(VisitLocatorInterface::class); + $this->visitToLocation = $this->prophesize(VisitToLocationHelperInterface::class); + + $this->listener = new LocateUnlocatedVisits($this->locator->reveal(), $this->visitToLocation->reveal()); + } + + /** @test */ + public function locatorIsCalledWhenInvoked(): void + { + ($this->listener)(new GeoLiteDbCreated()); + $this->locator->locateUnlocatedVisits($this->listener)->shouldHaveBeenCalledOnce(); + } + + /** @test */ + public function visitToLocationHelperIsCalledToGeolocateVisits(): void + { + $visit = Visit::forBasePath(Visitor::emptyInstance()); + $location = Location::emptyInstance(); + + $resolve = $this->visitToLocation->resolveVisitLocation($visit)->willReturn($location); + + $result = $this->listener->geolocateVisit($visit); + + self::assertSame($location, $result); + $resolve->shouldHaveBeenCalledOnce(); + } +} diff --git a/module/Core/test/Exception/IpCannotBeLocatedExceptionTest.php b/module/Core/test/Exception/IpCannotBeLocatedExceptionTest.php index b1487b69..2089daba 100644 --- a/module/Core/test/Exception/IpCannotBeLocatedExceptionTest.php +++ b/module/Core/test/Exception/IpCannotBeLocatedExceptionTest.php @@ -9,6 +9,7 @@ use LogicException; use PHPUnit\Framework\TestCase; use Shlinkio\Shlink\Core\Exception\IpCannotBeLocatedException; use Shlinkio\Shlink\Core\Exception\RuntimeException; +use Shlinkio\Shlink\Core\Visit\Model\UnlocatableIpType; use Throwable; class IpCannotBeLocatedExceptionTest extends TestCase @@ -22,6 +23,7 @@ class IpCannotBeLocatedExceptionTest extends TestCase self::assertEquals('Ignored visit with no IP address', $e->getMessage()); self::assertEquals(0, $e->getCode()); self::assertNull($e->getPrevious()); + self::assertEquals(UnlocatableIpType::EMPTY_ADDRESS, $e->type); } /** @test */ @@ -33,6 +35,7 @@ class IpCannotBeLocatedExceptionTest extends TestCase self::assertEquals('Ignored localhost address', $e->getMessage()); self::assertEquals(0, $e->getCode()); self::assertNull($e->getPrevious()); + self::assertEquals(UnlocatableIpType::LOCALHOST, $e->type); } /** @@ -47,6 +50,7 @@ class IpCannotBeLocatedExceptionTest extends TestCase self::assertEquals('An error occurred while locating IP', $e->getMessage()); self::assertEquals($prev->getCode(), $e->getCode()); self::assertSame($prev, $e->getPrevious()); + self::assertEquals(UnlocatableIpType::ERROR, $e->type); } public function provideErrors(): iterable diff --git a/module/Core/test/Visit/VisitLocatorTest.php b/module/Core/test/Visit/VisitLocatorTest.php index b740d143..21908be8 100644 --- a/module/Core/test/Visit/VisitLocatorTest.php +++ b/module/Core/test/Visit/VisitLocatorTest.php @@ -129,7 +129,7 @@ class VisitLocatorTest extends TestCase public function geolocateVisit(Visit $visit): Location { throw $this->isNonLocatableAddress - ? new IpCannotBeLocatedException('Cannot be located') + ? IpCannotBeLocatedException::forEmptyAddress() : IpCannotBeLocatedException::forError(new Exception('')); } diff --git a/module/Core/test/Visit/VisitToLocationHelperTest.php b/module/Core/test/Visit/VisitToLocationHelperTest.php new file mode 100644 index 00000000..ee22272f --- /dev/null +++ b/module/Core/test/Visit/VisitToLocationHelperTest.php @@ -0,0 +1,66 @@ +ipLocationResolver = $this->prophesize(IpLocationResolverInterface::class); + $this->helper = new VisitToLocationHelper($this->ipLocationResolver->reveal()); + } + + /** + * @test + * @dataProvider provideNonLocatableVisits + */ + public function throwsExpectedErrorForNonLocatableVisit( + Visit $visit, + IpCannotBeLocatedException $expectedException, + ): void { + $this->expectExceptionObject($expectedException); + $this->ipLocationResolver->resolveIpLocation(Argument::cetera())->shouldNotBeCalled(); + + $this->helper->resolveVisitLocation($visit); + } + + public function provideNonLocatableVisits(): iterable + { + yield [Visit::forBasePath(Visitor::emptyInstance()), IpCannotBeLocatedException::forEmptyAddress()]; + yield [ + Visit::forBasePath(new Visitor('foo', 'bar', IpAddress::LOCALHOST, '')), + IpCannotBeLocatedException::forLocalhost(), + ]; + } + + /** @test */ + public function throwsGenericErrorWhenResolvingIpFails(): void + { + $e = new WrongIpException(''); + + $this->expectExceptionObject(IpCannotBeLocatedException::forError($e)); + $this->ipLocationResolver->resolveIpLocation(Argument::cetera())->willThrow($e) + ->shouldBeCalledOnce(); + + $this->helper->resolveVisitLocation(Visit::forBasePath(new Visitor('foo', 'bar', '1.2.3.4', ''))); + } +} From 36680e82aa7433ff4772f1af21e36ac75902adb1 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 18 Sep 2022 19:21:59 +0200 Subject: [PATCH 106/108] Reduced duplication in LocateVisitsCommand by reusing VisitToLocationHelper --- module/CLI/config/dependencies.config.php | 3 +- .../src/Command/Visit/LocateVisitsCommand.php | 44 ++++++++----------- .../Command/Visit/LocateVisitsCommandTest.php | 39 +++++++--------- 3 files changed, 37 insertions(+), 49 deletions(-) diff --git a/module/CLI/config/dependencies.config.php b/module/CLI/config/dependencies.config.php index a9de1f0c..47a47735 100644 --- a/module/CLI/config/dependencies.config.php +++ b/module/CLI/config/dependencies.config.php @@ -20,7 +20,6 @@ use Shlinkio\Shlink\Core\Visit; use Shlinkio\Shlink\Installer\Factory\ProcessHelperFactory; use Shlinkio\Shlink\IpGeolocation\GeoLite2\DbUpdater; use Shlinkio\Shlink\IpGeolocation\GeoLite2\GeoLite2Options; -use Shlinkio\Shlink\IpGeolocation\Resolver\IpLocationResolverInterface; use Shlinkio\Shlink\Rest\Service\ApiKeyService; use Symfony\Component\Console as SymfonyCli; use Symfony\Component\Lock\LockFactory; @@ -97,7 +96,7 @@ return [ Command\Visit\DownloadGeoLiteDbCommand::class => [GeoLite\GeolocationDbUpdater::class], Command\Visit\LocateVisitsCommand::class => [ Visit\VisitLocator::class, - IpLocationResolverInterface::class, + Visit\VisitToLocationHelper::class, LockFactory::class, ], Command\Visit\GetOrphanVisitsCommand::class => [Visit\VisitsStatsHelper::class], diff --git a/module/CLI/src/Command/Visit/LocateVisitsCommand.php b/module/CLI/src/Command/Visit/LocateVisitsCommand.php index 47aa50d1..59db9367 100644 --- a/module/CLI/src/Command/Visit/LocateVisitsCommand.php +++ b/module/CLI/src/Command/Visit/LocateVisitsCommand.php @@ -11,11 +11,11 @@ use Shlinkio\Shlink\Common\Util\IpAddress; use Shlinkio\Shlink\Core\Entity\Visit; use Shlinkio\Shlink\Core\Entity\VisitLocation; use Shlinkio\Shlink\Core\Exception\IpCannotBeLocatedException; +use Shlinkio\Shlink\Core\Visit\Model\UnlocatableIpType; use Shlinkio\Shlink\Core\Visit\VisitGeolocationHelperInterface; use Shlinkio\Shlink\Core\Visit\VisitLocatorInterface; -use Shlinkio\Shlink\IpGeolocation\Exception\WrongIpException; +use Shlinkio\Shlink\Core\Visit\VisitToLocationHelperInterface; use Shlinkio\Shlink\IpGeolocation\Model\Location; -use Shlinkio\Shlink\IpGeolocation\Resolver\IpLocationResolverInterface; use Symfony\Component\Console\Exception\RuntimeException; use Symfony\Component\Console\Input\ArrayInput; use Symfony\Component\Console\Input\InputInterface; @@ -35,7 +35,7 @@ class LocateVisitsCommand extends AbstractLockedCommand implements VisitGeolocat public function __construct( private readonly VisitLocatorInterface $visitLocator, - private readonly IpLocationResolverInterface $ipLocationResolver, + private readonly VisitToLocationHelperInterface $visitToLocation, LockFactory $locker, ) { parent::__construct($locker); @@ -132,39 +132,33 @@ class LocateVisitsCommand extends AbstractLockedCommand implements VisitGeolocat */ public function geolocateVisit(Visit $visit): Location { - if (! $visit->hasRemoteAddr()) { - $this->io->writeln( - 'Ignored visit with no IP address', - OutputInterface::VERBOSITY_VERBOSE, - ); - throw IpCannotBeLocatedException::forEmptyAddress(); - } - - $ipAddr = $visit->getRemoteAddr() ?? ''; + $ipAddr = $visit->getRemoteAddr() ?? '?'; $this->io->write(sprintf('Processing IP %s', $ipAddr)); - if ($ipAddr === IpAddress::LOCALHOST) { - $this->io->writeln(' [Ignored localhost address]'); - throw IpCannotBeLocatedException::forLocalhost(); - } try { - return $this->ipLocationResolver->resolveIpLocation($ipAddr); - } catch (WrongIpException $e) { - $this->io->writeln(' [An error occurred while locating IP. Skipped]'); - if ($this->io->isVerbose()) { + return $this->visitToLocation->resolveVisitLocation($visit); + } catch (IpCannotBeLocatedException $e) { + $this->io->writeln(match ($e->type) { + UnlocatableIpType::EMPTY_ADDRESS => ' [Ignored visit with no IP address]', + UnlocatableIpType::LOCALHOST => ' [Ignored localhost address]', + UnlocatableIpType::ERROR => ' [An error occurred while locating IP. Skipped]', + }); + + if ($e->type === UnlocatableIpType::ERROR && $this->io->isVerbose()) { $this->getApplication()?->renderThrowable($e, $this->io); } - throw IpCannotBeLocatedException::forError($e); + throw $e; } } public function onVisitLocated(VisitLocation $visitLocation, Visit $visit): void { - $message = ! $visitLocation->isEmpty() - ? sprintf(' [Address located in "%s"]', $visitLocation->getCountryName()) - : ' [Address not found]'; - $this->io->writeln($message); + if (! $visitLocation->isEmpty()) { + $this->io->writeln(sprintf(' [Address located in "%s"]', $visitLocation->getCountryName())); + } elseif ($visit->hasRemoteAddr() && $visit->getRemoteAddr() !== IpAddress::LOCALHOST) { + $this->io->writeln(' [Could not locate address]'); + } } private function checkDbUpdate(): void diff --git a/module/CLI/test/Command/Visit/LocateVisitsCommandTest.php b/module/CLI/test/Command/Visit/LocateVisitsCommandTest.php index 4111c1dc..63ad3e52 100644 --- a/module/CLI/test/Command/Visit/LocateVisitsCommandTest.php +++ b/module/CLI/test/Command/Visit/LocateVisitsCommandTest.php @@ -10,16 +10,16 @@ use Prophecy\Prophecy\ObjectProphecy; use Shlinkio\Shlink\CLI\Command\Visit\DownloadGeoLiteDbCommand; use Shlinkio\Shlink\CLI\Command\Visit\LocateVisitsCommand; use Shlinkio\Shlink\CLI\Util\ExitCodes; -use Shlinkio\Shlink\Common\Util\IpAddress; use Shlinkio\Shlink\Core\Entity\ShortUrl; use Shlinkio\Shlink\Core\Entity\Visit; use Shlinkio\Shlink\Core\Entity\VisitLocation; +use Shlinkio\Shlink\Core\Exception\IpCannotBeLocatedException; use Shlinkio\Shlink\Core\Model\Visitor; use Shlinkio\Shlink\Core\Visit\VisitGeolocationHelperInterface; use Shlinkio\Shlink\Core\Visit\VisitLocator; +use Shlinkio\Shlink\Core\Visit\VisitToLocationHelperInterface; use Shlinkio\Shlink\IpGeolocation\Exception\WrongIpException; use Shlinkio\Shlink\IpGeolocation\Model\Location; -use Shlinkio\Shlink\IpGeolocation\Resolver\IpLocationResolverInterface; use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait; use Symfony\Component\Console\Exception\RuntimeException; use Symfony\Component\Console\Output\OutputInterface; @@ -36,14 +36,14 @@ class LocateVisitsCommandTest extends TestCase private CommandTester $commandTester; private ObjectProphecy $visitService; - private ObjectProphecy $ipResolver; + private ObjectProphecy $visitToLocation; private ObjectProphecy $lock; private ObjectProphecy $downloadDbCommand; protected function setUp(): void { $this->visitService = $this->prophesize(VisitLocator::class); - $this->ipResolver = $this->prophesize(IpLocationResolverInterface::class); + $this->visitToLocation = $this->prophesize(VisitToLocationHelperInterface::class); $locker = $this->prophesize(Lock\LockFactory::class); $this->lock = $this->prophesize(Lock\LockInterface::class); @@ -54,7 +54,7 @@ class LocateVisitsCommandTest extends TestCase $command = new LocateVisitsCommand( $this->visitService->reveal(), - $this->ipResolver->reveal(), + $this->visitToLocation->reveal(), $locker->reveal(), ); @@ -84,7 +84,7 @@ class LocateVisitsCommandTest extends TestCase $mockMethodBehavior, ); $locateAllVisits = $this->visitService->locateAllVisits(Argument::cetera())->will($mockMethodBehavior); - $resolveIpLocation = $this->ipResolver->resolveIpLocation(Argument::any())->willReturn( + $resolveIpLocation = $this->visitToLocation->resolveVisitLocation(Argument::any())->willReturn( Location::emptyInstance(), ); @@ -117,36 +117,29 @@ class LocateVisitsCommandTest extends TestCase * @test * @dataProvider provideIgnoredAddresses */ - public function localhostAndEmptyAddressesAreIgnored(?string $address, string $message): void + public function localhostAndEmptyAddressesAreIgnored(IpCannotBeLocatedException $e, string $message): void { - $visit = Visit::forValidShortUrl(ShortUrl::createEmpty(), new Visitor('', '', $address, '')); + $visit = Visit::forValidShortUrl(ShortUrl::createEmpty(), Visitor::emptyInstance()); $location = VisitLocation::fromGeolocation(Location::emptyInstance()); $locateVisits = $this->visitService->locateUnlocatedVisits(Argument::cetera())->will( $this->invokeHelperMethods($visit, $location), ); - $resolveIpLocation = $this->ipResolver->resolveIpLocation(Argument::any())->willReturn( - Location::emptyInstance(), - ); + $resolveIpLocation = $this->visitToLocation->resolveVisitLocation(Argument::any())->willThrow($e); $this->commandTester->execute([], ['verbosity' => OutputInterface::VERBOSITY_VERBOSE]); $output = $this->commandTester->getDisplay(); + self::assertStringContainsString('Processing IP', $output); self::assertStringContainsString($message, $output); - if (empty($address)) { - self::assertStringNotContainsString('Processing IP', $output); - } else { - self::assertStringContainsString('Processing IP', $output); - } $locateVisits->shouldHaveBeenCalledOnce(); - $resolveIpLocation->shouldNotHaveBeenCalled(); + $resolveIpLocation->shouldHaveBeenCalledOnce(); } public function provideIgnoredAddresses(): iterable { - yield 'with empty address' => ['', 'Ignored visit with no IP address']; - yield 'with null address' => [null, 'Ignored visit with no IP address']; - yield 'with localhost address' => [IpAddress::LOCALHOST, 'Ignored localhost address']; + yield 'empty address' => [IpCannotBeLocatedException::forEmptyAddress(), 'Ignored visit with no IP address']; + yield 'localhost address' => [IpCannotBeLocatedException::forLocalhost(), 'Ignored localhost address']; } /** @test */ @@ -158,7 +151,9 @@ class LocateVisitsCommandTest extends TestCase $locateVisits = $this->visitService->locateUnlocatedVisits(Argument::cetera())->will( $this->invokeHelperMethods($visit, $location), ); - $resolveIpLocation = $this->ipResolver->resolveIpLocation(Argument::any())->willThrow(WrongIpException::class); + $resolveIpLocation = $this->visitToLocation->resolveVisitLocation(Argument::any())->willThrow( + IpCannotBeLocatedException::forError(WrongIpException::fromIpAddress('1.2.3.4')), + ); $this->commandTester->execute([], ['verbosity' => OutputInterface::VERBOSITY_VERBOSE]); @@ -187,7 +182,7 @@ class LocateVisitsCommandTest extends TestCase $locateVisits = $this->visitService->locateUnlocatedVisits(Argument::cetera())->will(function (): void { }); - $resolveIpLocation = $this->ipResolver->resolveIpLocation(Argument::any())->willReturn([]); + $resolveIpLocation = $this->visitToLocation->resolveVisitLocation(Argument::any()); $this->commandTester->execute([], ['verbosity' => OutputInterface::VERBOSITY_VERBOSE]); $output = $this->commandTester->getDisplay(); From 8605b35b576171a9acb1dd863dc6489c5ded66d8 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 18 Sep 2022 19:22:57 +0200 Subject: [PATCH 107/108] Removed unneeded injected dependency --- module/CLI/config/dependencies.config.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/module/CLI/config/dependencies.config.php b/module/CLI/config/dependencies.config.php index 47a47735..dffc6010 100644 --- a/module/CLI/config/dependencies.config.php +++ b/module/CLI/config/dependencies.config.php @@ -19,7 +19,6 @@ use Shlinkio\Shlink\Core\Tag\TagService; use Shlinkio\Shlink\Core\Visit; use Shlinkio\Shlink\Installer\Factory\ProcessHelperFactory; use Shlinkio\Shlink\IpGeolocation\GeoLite2\DbUpdater; -use Shlinkio\Shlink\IpGeolocation\GeoLite2\GeoLite2Options; use Shlinkio\Shlink\Rest\Service\ApiKeyService; use Symfony\Component\Console as SymfonyCli; use Symfony\Component\Lock\LockFactory; @@ -75,7 +74,6 @@ return [ Reader::class, LOCAL_LOCK_FACTORY, TrackingOptions::class, - GeoLite2Options::class, ], Util\ProcessRunner::class => [SymfonyCli\Helper\ProcessHelper::class], ApiKey\RoleResolver::class => [DomainService::class, 'config.url_shortener.domain.hostname'], From 68e1c61e7fc9757350fa12c33ff735e6c5a33d13 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 18 Sep 2022 19:28:14 +0200 Subject: [PATCH 108/108] Removed unnecessary ADR entry --- ...low-delaying-initial-geolite2-db-download-in-docker-images.md | 0 docs/adr/README.md | 1 - 2 files changed, 1 deletion(-) delete mode 100644 docs/adr/2022-09-18-allow-delaying-initial-geolite2-db-download-in-docker-images.md diff --git a/docs/adr/2022-09-18-allow-delaying-initial-geolite2-db-download-in-docker-images.md b/docs/adr/2022-09-18-allow-delaying-initial-geolite2-db-download-in-docker-images.md deleted file mode 100644 index e69de29b..00000000 diff --git a/docs/adr/README.md b/docs/adr/README.md index f12d17c1..7cfccdf7 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -2,7 +2,6 @@ Here listed you will find the different architectural decisions taken in the project, including all the reasoning behind it, options considered, and final outcome. -* [2022-09-18 Allow delaying initial GeoLite2 DB download in docker images](2022-09-18-allow-delaying-initial-geolite2-db-download-in-docker-images.md) * [2022-08-05 Support multi-segment custom slugs](2022-08-05-support-multi-segment-custom-slugs.md) * [2022-01-15 Update env vars behavior to have precedence over installer options](2022-01-15-update-env-vars-behavior-to-have-precedence-over-installer-options.md) * [2021-08-05 Migrate to a new caching library](2021-08-05-migrate-to-a-new-caching-library.md)