From eb1345e5c31793ce43e7dcdbcfd019f870b3b87f Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 31 Oct 2021 13:02:58 +0100 Subject: [PATCH 01/66] Updated to symfony/mercure 0.6 --- CHANGELOG.md | 17 +++++++++++++++++ composer.json | 4 ++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f895b28b..8a7e0a98 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 +* [#1218](https://github.com/shlinkio/shlink/issues/1218) Updated to symfony/mercure 0.6. + +### Deprecated +* *Nothing* + +### Removed +* *Nothing* + +### Fixed +* *Nothing* + + ## [2.9.2] - 2021-10-23 ### Added * *Nothing* diff --git a/composer.json b/composer.json index 118ae244..10cc0974 100644 --- a/composer.json +++ b/composer.json @@ -47,7 +47,7 @@ "pugx/shortid-php": "^0.7", "ramsey/uuid": "^3.9", "rlanvin/php-ip": "3.0.0-rc2", - "shlinkio/shlink-common": "^4.0", + "shlinkio/shlink-common": "dev-main#0f935d4 as 4.1", "shlinkio/shlink-config": "^1.2", "shlinkio/shlink-event-dispatcher": "^2.1", "shlinkio/shlink-importer": "^2.3.1", @@ -56,7 +56,7 @@ "symfony/console": "^5.3", "symfony/filesystem": "^5.3", "symfony/lock": "^5.3", - "symfony/mercure": "^0.5.3", + "symfony/mercure": "^0.6", "symfony/process": "^5.3", "symfony/string": "^5.3" }, From 93a3d78111f4311122fefd5274efcc3d3cb6b4d5 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 31 Oct 2021 19:42:40 +0100 Subject: [PATCH 02/66] Updated mercure on dev env from v0.10 to 0.13 --- docker-compose.yml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index ab7baf1f..42b0f3a0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -131,10 +131,11 @@ services: shlink_mercure: container_name: shlink_mercure - image: dunglas/mercure:v0.10 + image: dunglas/mercure:v0.13 ports: - "3080:80" environment: - CORS_ALLOWED_ORIGINS: "*" - JWT_KEY: "mercure_jwt_key" - USE_FORWARDED_HEADERS: "1" + SERVER_NAME: ":80" + MERCURE_PUBLISHER_JWT_KEY: mercure_jwt_key + MERCURE_SUBSCRIBER_JWT_KEY: mercure_jwt_key + MERCURE_EXTRA_DIRECTIVES: "cors_origins https://app.shlink.io http://localhost:3000 http://127.0.0.1:3000" From ac89f352cea984ba5cb2aa0a7532cfdf363d7766 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Mon, 1 Nov 2021 11:27:44 +0100 Subject: [PATCH 03/66] Updated shlink libs --- composer.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index 10cc0974..7abcf315 100644 --- a/composer.json +++ b/composer.json @@ -48,11 +48,11 @@ "ramsey/uuid": "^3.9", "rlanvin/php-ip": "3.0.0-rc2", "shlinkio/shlink-common": "dev-main#0f935d4 as 4.1", - "shlinkio/shlink-config": "^1.2", - "shlinkio/shlink-event-dispatcher": "^2.1", + "shlinkio/shlink-config": "^1.3.1", + "shlinkio/shlink-event-dispatcher": "^2.2", "shlinkio/shlink-importer": "^2.3.1", "shlinkio/shlink-installer": "^6.2.1", - "shlinkio/shlink-ip-geolocation": "^2.0", + "shlinkio/shlink-ip-geolocation": "^2.1", "symfony/console": "^5.3", "symfony/filesystem": "^5.3", "symfony/lock": "^5.3", From da76eb5cf41fd188e72abf19a63f3c97fc944b8c Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Thu, 4 Nov 2021 21:17:31 +0100 Subject: [PATCH 04/66] Updated to phpstan 1.0 --- CHANGELOG.md | 1 + composer.json | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a7e0a98..fc24d045 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 * [#1218](https://github.com/shlinkio/shlink/issues/1218) Updated to symfony/mercure 0.6. +* [#1223](https://github.com/shlinkio/shlink/issues/1223) Updated to phpstan 1.0. ### Deprecated * *Nothing* diff --git a/composer.json b/composer.json index 7abcf315..880f9a6b 100644 --- a/composer.json +++ b/composer.json @@ -66,9 +66,9 @@ "eaglewu/swoole-ide-helper": "dev-master", "infection/infection": "^0.25.0", "phpspec/prophecy-phpunit": "^2.0", - "phpstan/phpstan": "^0.12.94", - "phpstan/phpstan-doctrine": "^0.12.42", - "phpstan/phpstan-symfony": "^0.12.41", + "phpstan/phpstan": "^1.0", + "phpstan/phpstan-doctrine": "^1.0", + "phpstan/phpstan-symfony": "^1.0", "phpunit/php-code-coverage": "^9.2", "phpunit/phpunit": "^9.5", "roave/security-advisories": "dev-master", From f532b5edeee0b07a4b414c5ab4895f07120fd728 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Thu, 4 Nov 2021 21:31:51 +0100 Subject: [PATCH 05/66] Added LC_ALL: C env var during ms db tests --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9d2067da..cf252eb9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -182,6 +182,8 @@ jobs: matrix: php-version: ['8.0', '8.1'] continue-on-error: ${{ matrix.php-version == '8.1' }} + env: + LC_ALL: C steps: - name: Checkout code uses: actions/checkout@v2 From a66ddabe8abbb0459efcb613723662a1565b1076 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Tue, 30 Nov 2021 21:37:35 +0100 Subject: [PATCH 06/66] Added domain to DeleteShortUrlException --- CHANGELOG.md | 1 + .../ShortUrl/DeleteShortUrlCommandTest.php | 14 +++++++--- .../src/Exception/DeleteShortUrlException.php | 13 +++++++-- .../ShortUrl/DeleteShortUrlService.php | 2 +- .../Exception/DeleteShortUrlExceptionTest.php | 27 +++++++++++++++++-- .../ShortUrl/DeleteShortUrlServiceTest.php | 2 +- .../test-api/Action/DeleteShortUrlTest.php | 3 ++- 7 files changed, 51 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2002a898..57d95a4a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this ### Changed * [#1218](https://github.com/shlinkio/shlink/issues/1218) Updated to symfony/mercure 0.6. * [#1223](https://github.com/shlinkio/shlink/issues/1223) Updated to phpstan 1.0. +* Added `domain` field to `DeleteShortUrlException` exception. ### Deprecated * *Nothing* diff --git a/module/CLI/test/Command/ShortUrl/DeleteShortUrlCommandTest.php b/module/CLI/test/Command/ShortUrl/DeleteShortUrlCommandTest.php index 765a1c4b..10a363c7 100644 --- a/module/CLI/test/Command/ShortUrl/DeleteShortUrlCommandTest.php +++ b/module/CLI/test/Command/ShortUrl/DeleteShortUrlCommandTest.php @@ -83,7 +83,10 @@ class DeleteShortUrlCommandTest extends TestCase $ignoreThreshold = array_pop($args); if (!$ignoreThreshold) { - throw Exception\DeleteShortUrlException::fromVisitsThreshold(10, $shortCode); + throw Exception\DeleteShortUrlException::fromVisitsThreshold( + 10, + ShortUrlIdentifier::fromShortCodeAndDomain($shortCode), + ); } }, ); @@ -93,7 +96,7 @@ class DeleteShortUrlCommandTest extends TestCase $output = $this->commandTester->getDisplay(); self::assertStringContainsString(sprintf( - 'Impossible to delete short URL with short code "%s" since it has more than "10" visits.', + 'Impossible to delete short URL with short code "%s", since it has more than "10" visits.', $shortCode, ), $output); self::assertStringContainsString($expectedMessage, $output); @@ -112,7 +115,10 @@ class DeleteShortUrlCommandTest extends TestCase { $shortCode = 'abc123'; $deleteByShortCode = $this->service->deleteByShortCode(new ShortUrlIdentifier($shortCode), false)->willThrow( - Exception\DeleteShortUrlException::fromVisitsThreshold(10, $shortCode), + Exception\DeleteShortUrlException::fromVisitsThreshold( + 10, + ShortUrlIdentifier::fromShortCodeAndDomain($shortCode), + ), ); $this->commandTester->setInputs(['no']); @@ -120,7 +126,7 @@ class DeleteShortUrlCommandTest extends TestCase $output = $this->commandTester->getDisplay(); self::assertStringContainsString(sprintf( - 'Impossible to delete short URL with short code "%s" since it has more than "10" visits.', + 'Impossible to delete short URL with short code "%s", since it has more than "10" visits.', $shortCode, ), $output); self::assertStringContainsString('Short URL was not deleted.', $output); diff --git a/module/Core/src/Exception/DeleteShortUrlException.php b/module/Core/src/Exception/DeleteShortUrlException.php index 600fca57..98919b35 100644 --- a/module/Core/src/Exception/DeleteShortUrlException.php +++ b/module/Core/src/Exception/DeleteShortUrlException.php @@ -7,6 +7,7 @@ namespace Shlinkio\Shlink\Core\Exception; use Fig\Http\Message\StatusCodeInterface; use Mezzio\ProblemDetails\Exception\CommonProblemDetailsExceptionTrait; use Mezzio\ProblemDetails\Exception\ProblemDetailsExceptionInterface; +use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier; use function sprintf; @@ -17,11 +18,15 @@ class DeleteShortUrlException extends DomainException implements ProblemDetailsE private const TITLE = 'Cannot delete short URL'; private const TYPE = 'INVALID_SHORTCODE_DELETION'; // FIXME Deprecated: Should be INVALID_SHORT_URL_DELETION - public static function fromVisitsThreshold(int $threshold, string $shortCode): self + public static function fromVisitsThreshold(int $threshold, ShortUrlIdentifier $identifier): self { + $shortCode = $identifier->shortCode(); + $domain = $identifier->domain(); + $suffix = $domain === null ? '' : sprintf(' for domain "%s"', $domain); $e = new self(sprintf( - 'Impossible to delete short URL with short code "%s" since it has more than "%s" visits.', + 'Impossible to delete short URL with short code "%s"%s, since it has more than "%s" visits.', $shortCode, + $suffix, $threshold, )); @@ -34,6 +39,10 @@ class DeleteShortUrlException extends DomainException implements ProblemDetailsE 'threshold' => $threshold, ]; + if ($domain !== null) { + $e->additional['domain'] = $domain; + } + return $e; } diff --git a/module/Core/src/Service/ShortUrl/DeleteShortUrlService.php b/module/Core/src/Service/ShortUrl/DeleteShortUrlService.php index 0732b737..e6f2e82d 100644 --- a/module/Core/src/Service/ShortUrl/DeleteShortUrlService.php +++ b/module/Core/src/Service/ShortUrl/DeleteShortUrlService.php @@ -33,7 +33,7 @@ class DeleteShortUrlService implements DeleteShortUrlServiceInterface if (! $ignoreThreshold && $this->isThresholdReached($shortUrl)) { throw Exception\DeleteShortUrlException::fromVisitsThreshold( $this->deleteShortUrlsOptions->getVisitsThreshold(), - $shortUrl->getShortCode(), + $identifier, ); } diff --git a/module/Core/test/Exception/DeleteShortUrlExceptionTest.php b/module/Core/test/Exception/DeleteShortUrlExceptionTest.php index 43dcc2e5..8c616ce1 100644 --- a/module/Core/test/Exception/DeleteShortUrlExceptionTest.php +++ b/module/Core/test/Exception/DeleteShortUrlExceptionTest.php @@ -6,6 +6,7 @@ namespace ShlinkioTest\Shlink\Core\Exception; use PHPUnit\Framework\TestCase; use Shlinkio\Shlink\Core\Exception\DeleteShortUrlException; +use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier; use function Functional\map; use function range; @@ -23,7 +24,10 @@ class DeleteShortUrlExceptionTest extends TestCase string $shortCode, string $expectedMessage, ): void { - $e = DeleteShortUrlException::fromVisitsThreshold($threshold, $shortCode); + $e = DeleteShortUrlException::fromVisitsThreshold( + $threshold, + ShortUrlIdentifier::fromShortCodeAndDomain($shortCode), + ); self::assertEquals($threshold, $e->getVisitsThreshold()); self::assertEquals($expectedMessage, $e->getMessage()); @@ -41,10 +45,29 @@ class DeleteShortUrlExceptionTest extends TestCase { return map(range(5, 50, 5), function (int $number) { return [$number, $shortCode = generateRandomShortCode(6), sprintf( - 'Impossible to delete short URL with short code "%s" since it has more than "%s" visits.', + 'Impossible to delete short URL with short code "%s", since it has more than "%s" visits.', $shortCode, $number, )]; }); } + + /** @test */ + public function domainIsPartOfAdditionalWhenProvidedInIdentifier(): void + { + $e = DeleteShortUrlException::fromVisitsThreshold( + 10, + ShortUrlIdentifier::fromShortCodeAndDomain('abc123', 'doma.in'), + ); + $expectedMessage = 'Impossible to delete short URL with short code "abc123" for domain "doma.in", since it ' + . 'has more than "10" visits.'; + + self::assertEquals([ + 'shortCode' => 'abc123', + 'domain' => 'doma.in', + 'threshold' => 10, + ], $e->getAdditionalData()); + self::assertEquals($expectedMessage, $e->getMessage()); + self::assertEquals($expectedMessage, $e->getDetail()); + } } diff --git a/module/Core/test/Service/ShortUrl/DeleteShortUrlServiceTest.php b/module/Core/test/Service/ShortUrl/DeleteShortUrlServiceTest.php index 4c066848..6c03d7b5 100644 --- a/module/Core/test/Service/ShortUrl/DeleteShortUrlServiceTest.php +++ b/module/Core/test/Service/ShortUrl/DeleteShortUrlServiceTest.php @@ -51,7 +51,7 @@ class DeleteShortUrlServiceTest extends TestCase $this->expectException(DeleteShortUrlException::class); $this->expectExceptionMessage(sprintf( - 'Impossible to delete short URL with short code "%s" since it has more than "5" visits.', + 'Impossible to delete short URL with short code "%s", since it has more than "5" visits.', $this->shortCode, )); diff --git a/module/Rest/test-api/Action/DeleteShortUrlTest.php b/module/Rest/test-api/Action/DeleteShortUrlTest.php index 479527c1..bb512832 100644 --- a/module/Rest/test-api/Action/DeleteShortUrlTest.php +++ b/module/Rest/test-api/Action/DeleteShortUrlTest.php @@ -40,7 +40,8 @@ class DeleteShortUrlTest extends ApiTestCase for ($i = 0; $i < 20; $i++) { self::assertEquals(self::STATUS_FOUND, $this->callShortUrl('abc123')->getStatusCode()); } - $expectedDetail = 'Impossible to delete short URL with short code "abc123" since it has more than "15" visits.'; + $expectedDetail = 'Impossible to delete short URL with short code "abc123", since it has more than "15" ' + . 'visits.'; $resp = $this->callApiWithKey(self::METHOD_DELETE, '/short-urls/abc123'); $payload = $this->getJsonResponsePayload($resp); From a83ae996db21ee8e3a18eb958c03325a4e61f229 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Tue, 30 Nov 2021 21:47:23 +0100 Subject: [PATCH 07/66] Ensured a formatter is resolved --- module/CLI/src/Util/ProcessRunner.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/module/CLI/src/Util/ProcessRunner.php b/module/CLI/src/Util/ProcessRunner.php index 66e94eb6..1a5471e5 100644 --- a/module/CLI/src/Util/ProcessRunner.php +++ b/module/CLI/src/Util/ProcessRunner.php @@ -34,7 +34,7 @@ class ProcessRunner implements ProcessRunnerInterface } /** @var DebugFormatterHelper $formatter */ - $formatter = $this->helper->getHelperSet()->get('debug_formatter'); + $formatter = $this->helper->getHelperSet()?->get('debug_formatter') ?? new DebugFormatterHelper(); /** @var Process $process */ $process = ($this->createProcess)($cmd); From c0dcd3181907f8a6d6ec65c0ce095b7a96d160b0 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Thu, 2 Dec 2021 19:33:01 +0100 Subject: [PATCH 08/66] Update ci.yml --- .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 cf252eb9..4d333107 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -197,7 +197,7 @@ jobs: with: php-version: ${{ matrix.php-version }} tools: composer - extensions: swoole-4.7.1, pdo_sqlsrv-5.10.0beta1 + extensions: swoole-4.7.1, pdo_sqlsrv-5.10.0beta2 coverage: none - name: Use PHP if: ${{ matrix.php-version != '8.1' }} From 8afe058cfc5e1fd7ecacb5cfab536fdf5f1e6860 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Thu, 2 Dec 2021 20:55:50 +0100 Subject: [PATCH 09/66] Updated dependencies --- .github/workflows/ci.yml | 9 ------ Dockerfile | 2 +- composer.json | 61 ++++++++++++++++++------------------ data/infra/php.Dockerfile | 2 +- data/infra/swoole.Dockerfile | 2 +- 5 files changed, 33 insertions(+), 43 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4d333107..778f80c3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -192,21 +192,12 @@ jobs: - name: Start database server run: docker-compose -f docker-compose.yml -f docker-compose.ci.yml up -d shlink_db_ms - name: Use PHP - if: ${{ matrix.php-version == '8.1' }} uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php-version }} tools: composer extensions: swoole-4.7.1, pdo_sqlsrv-5.10.0beta2 coverage: none - - name: Use PHP - if: ${{ matrix.php-version != '8.1' }} - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php-version }} - tools: composer - extensions: swoole-4.7.1, pdo_sqlsrv-5.9.0 - coverage: none - if: ${{ matrix.php-version == '8.1' }} run: composer install --no-interaction --prefer-dist --ignore-platform-req=php - if: ${{ matrix.php-version != '8.1' }} diff --git a/Dockerfile b/Dockerfile index aea95a86..91d0361f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,7 +3,7 @@ FROM php:8.0.9-alpine3.14 as base ARG SHLINK_VERSION=latest ENV SHLINK_VERSION ${SHLINK_VERSION} ENV SWOOLE_VERSION 4.7.1 -ENV PDO_SQLSRV_VERSION 5.9.0 +ENV PDO_SQLSRV_VERSION 5.10.0beta2 ENV MS_ODBC_SQL_VERSION 17.5.2.2 ENV LC_ALL "C" diff --git a/composer.json b/composer.json index 1d1eaa07..94d2536f 100644 --- a/composer.json +++ b/composer.json @@ -15,30 +15,29 @@ "php": "^8.0", "ext-json": "*", "ext-pdo": "*", - "akrabat/ip-address-middleware": "^2.0", - "cakephp/chronos": "^2.2", + "akrabat/ip-address-middleware": "^2.1", + "cakephp/chronos": "^2.3", "cocur/slugify": "^4.0", - "doctrine/dbal": "^3.1.4", - "doctrine/migrations": "^3.3 <3.3.2", - "doctrine/orm": "^2.9", - "endroid/qr-code": "^4.2", - "geoip2/geoip2": "^2.11", - "guzzlehttp/guzzle": "^7.3", + "doctrine/migrations": "^3.3", + "doctrine/orm": "^2.10", + "endroid/qr-code": "^4.4", + "geoip2/geoip2": "^2.12", + "guzzlehttp/guzzle": "^7.4", "happyr/doctrine-specification": "^2.0", "jaybizzle/crawler-detect": "^1.2", - "laminas/laminas-config": "^3.5", - "laminas/laminas-config-aggregator": "^1.5", - "laminas/laminas-diactoros": "^2.6", - "laminas/laminas-inputfilter": "^2.12", - "laminas/laminas-servicemanager": "^3.7", - "laminas/laminas-stdlib": "^3.5", + "laminas/laminas-config": "^3.7", + "laminas/laminas-config-aggregator": "^1.7", + "laminas/laminas-diactoros": "^2.8", + "laminas/laminas-inputfilter": "^2.13", + "laminas/laminas-servicemanager": "^3.10", + "laminas/laminas-stdlib": "^3.6", "lcobucci/jwt": "^4.1", "league/uri": "^6.4", "lstrojny/functional-php": "^1.17", - "mezzio/mezzio": "^3.5", - "mezzio/mezzio-fastroute": "^3.2", - "mezzio/mezzio-problem-details": "^1.4", - "mezzio/mezzio-swoole": "^3.3", + "mezzio/mezzio": "^3.7", + "mezzio/mezzio-fastroute": "^3.3", + "mezzio/mezzio-problem-details": "^1.5", + "mezzio/mezzio-swoole": "^3.5", "monolog/monolog": "^2.3", "nikolaposa/monolog-factory": "^3.1", "ocramius/proxy-manager": "^2.11", @@ -48,35 +47,35 @@ "pugx/shortid-php": "^0.7", "ramsey/uuid": "^3.9", "rlanvin/php-ip": "3.0.0-rc2", - "shlinkio/shlink-common": "dev-main#0f935d4 as 4.1", + "shlinkio/shlink-common": "^4.1", "shlinkio/shlink-config": "^1.3.1", "shlinkio/shlink-event-dispatcher": "^2.2", - "shlinkio/shlink-importer": "^2.3.1", + "shlinkio/shlink-importer": "^2.4", "shlinkio/shlink-installer": "^6.2.1", - "shlinkio/shlink-ip-geolocation": "^2.1", - "symfony/console": "^5.3", - "symfony/filesystem": "^5.3", - "symfony/lock": "^5.3", + "shlinkio/shlink-ip-geolocation": "^2.2", + "symfony/console": "^5.4", + "symfony/filesystem": "^5.4", + "symfony/lock": "^5.4", "symfony/mercure": "^0.6", - "symfony/process": "^5.3", - "symfony/string": "^5.3" + "symfony/process": "^5.4", + "symfony/string": "^5.4" }, "require-dev": { "devster/ubench": "^2.1", "dms/phpunit-arraysubset-asserts": "^0.3.0", "eaglewu/swoole-ide-helper": "dev-master", - "infection/infection": "^0.25.0", + "infection/infection": "^0.25.3", "phpspec/prophecy-phpunit": "^2.0", - "phpstan/phpstan": "^1.0", + "phpstan/phpstan": "^1.2", "phpstan/phpstan-doctrine": "^1.0", "phpstan/phpstan-symfony": "^1.0", "phpunit/php-code-coverage": "^9.2", "phpunit/phpunit": "^9.5", "roave/security-advisories": "dev-master", "shlinkio/php-coding-standard": "~2.2.0", - "shlinkio/shlink-test-utils": "^2.3", - "symfony/var-dumper": "^5.3", - "veewee/composer-run-parallel": "^1.0" + "shlinkio/shlink-test-utils": "^2.4", + "symfony/var-dumper": "^6.0", + "veewee/composer-run-parallel": "^1.1" }, "autoload": { "psr-4": { diff --git a/data/infra/php.Dockerfile b/data/infra/php.Dockerfile index 1503ddf2..fe6c2722 100644 --- a/data/infra/php.Dockerfile +++ b/data/infra/php.Dockerfile @@ -2,7 +2,7 @@ FROM php:8.0.9-fpm-alpine3.14 MAINTAINER Alejandro Celaya ENV APCU_VERSION 5.1.20 -ENV PDO_SQLSRV_VERSION 5.9.0 +ENV PDO_SQLSRV_VERSION 5.10.0beta2 ENV MS_ODBC_SQL_VERSION 17.5.2.2 RUN apk update diff --git a/data/infra/swoole.Dockerfile b/data/infra/swoole.Dockerfile index 5b4fac1c..0ceb2030 100644 --- a/data/infra/swoole.Dockerfile +++ b/data/infra/swoole.Dockerfile @@ -4,7 +4,7 @@ MAINTAINER Alejandro Celaya ENV APCU_VERSION 5.1.20 ENV INOTIFY_VERSION 3.0.0 ENV SWOOLE_VERSION 4.7.1 -ENV PDO_SQLSRV_VERSION 5.9.0 +ENV PDO_SQLSRV_VERSION 5.10.0beta2 ENV MS_ODBC_SQL_VERSION 17.5.2.2 RUN apk update From 7477e672fe6848607f654e6b642f4382f09ce443 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 5 Dec 2021 08:55:05 +0100 Subject: [PATCH 10/66] Added mutation score badge --- .github/workflows/ci.yml | 7 ++++++- README.md | 1 + infection.json | 5 ++++- module/Core/src/Tag/Model/TagRenaming.php | 11 ++--------- 4 files changed, 13 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 778f80c3..8c8d7d47 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -267,7 +267,12 @@ jobs: - uses: actions/download-artifact@v2 with: path: build - - run: composer infect:ci:${{ matrix.test-group }} + - if: ${{ matrix.test-group == 'unit' }} + run: composer infect:ci:unit + env: + INFECTION_BADGE_API_KEY: ${{ secrets.INFECTION_BADGE_API_KEY }} + - if: ${{ matrix.test-group == 'db' }} + run: composer infect:ci:db upload-coverage: needs: diff --git a/README.md b/README.md index cd15bb5c..ce91829f 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ [![Build Status](https://img.shields.io/github/workflow/status/shlinkio/shlink/Continuous%20integration/develop?logo=github&style=flat-square)](https://github.com/shlinkio/shlink/actions?query=workflow%3A%22Continuous+integration%22) [![Code Coverage](https://img.shields.io/codecov/c/gh/shlinkio/shlink/develop?style=flat-square)](https://app.codecov.io/gh/shlinkio/shlink) +[![Infection MSI](https://img.shields.io/endpoint?style=flat-square&url=https%3A%2F%2Fbadge-api.stryker-mutator.io%2Fgithub.com%2Fshlinkio%2Fshlink%2Fdevelop)](https://dashboard.stryker-mutator.io/reports/github.com/shlinkio/shlink/develop) [![Latest Stable Version](https://img.shields.io/github/release/shlinkio/shlink.svg?style=flat-square)](https://packagist.org/packages/shlinkio/shlink) [![Docker pulls](https://img.shields.io/docker/pulls/shlinkio/shlink.svg?logo=docker&style=flat-square)](https://hub.docker.com/r/shlinkio/shlink/) [![License](https://img.shields.io/github/license/shlinkio/shlink.svg?style=flat-square)](https://github.com/shlinkio/shlink/blob/main/LICENSE) diff --git a/infection.json b/infection.json index b182bddf..1b4ed6b5 100644 --- a/infection.json +++ b/infection.json @@ -8,7 +8,10 @@ "logs": { "text": "build/infection-unit/infection-log.txt", "summary": "build/infection-unit/summary-log.txt", - "debug": "build/infection-unit/debug-log.txt" + "debug": "build/infection-unit/debug-log.txt", + "badge": { + "branch": "develop" + } }, "tmpDir": "build/infection-unit/temp", "phpUnit": { diff --git a/module/Core/src/Tag/Model/TagRenaming.php b/module/Core/src/Tag/Model/TagRenaming.php index 1f677376..3bdae21c 100644 --- a/module/Core/src/Tag/Model/TagRenaming.php +++ b/module/Core/src/Tag/Model/TagRenaming.php @@ -10,20 +10,13 @@ use function sprintf; final class TagRenaming { - private string $oldName; - private string $newName; - - private function __construct() + private function __construct(private string $oldName, private string $newName) { } public static function fromNames(string $oldName, string $newName): self { - $o = new self(); - $o->oldName = $oldName; - $o->newName = $newName; - - return $o; + return new self($oldName, $newName); } public static function fromArray(array $payload): self From 7e74d06cdd115af6db16636af987026c6a11c66b Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 5 Dec 2021 10:08:10 +0100 Subject: [PATCH 11/66] Added support for openswoole and migrated docker images from swoole to openswoole --- .github/workflows/ci.yml | 20 ++++++++++---------- .github/workflows/publish-release.yml | 2 +- Dockerfile | 10 +++++----- README.md | 4 ++-- composer.json | 8 ++++---- config/autoload/logger.local.php.dist | 2 +- config/autoload/url-shortener.local.php.dist | 2 +- data/infra/swoole.Dockerfile | 12 ++++++------ docker/README.md | 2 +- docker/docker-entrypoint.sh | 2 +- 10 files changed, 32 insertions(+), 32 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8c8d7d47..94ebcc9e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,7 +21,7 @@ jobs: with: php-version: ${{ matrix.php-version }} tools: composer - extensions: swoole-4.7.1 + extensions: openswoole-4.8.1 coverage: none - run: composer install --no-interaction --prefer-dist - run: composer cs @@ -39,7 +39,7 @@ jobs: with: php-version: ${{ matrix.php-version }} tools: composer - extensions: swoole-4.7.1 + extensions: openswoole-4.8.1 coverage: none - run: composer install --no-interaction --prefer-dist - run: composer stan @@ -58,7 +58,7 @@ jobs: with: php-version: ${{ matrix.php-version }} tools: composer - extensions: swoole-4.7.1 + extensions: openswoole-4.8.1 coverage: pcov ini-values: pcov.directory=module - if: ${{ matrix.php-version == '8.1' }} @@ -88,7 +88,7 @@ jobs: with: php-version: ${{ matrix.php-version }} tools: composer - extensions: swoole-4.7.1 + extensions: openswoole-4.8.1 coverage: pcov ini-values: pcov.directory=module - if: ${{ matrix.php-version == '8.1' }} @@ -120,7 +120,7 @@ jobs: with: php-version: ${{ matrix.php-version }} tools: composer - extensions: swoole-4.7.1 + extensions: openswoole-4.8.1 coverage: none - if: ${{ matrix.php-version == '8.1' }} run: composer install --no-interaction --prefer-dist --ignore-platform-req=php @@ -144,7 +144,7 @@ jobs: with: php-version: ${{ matrix.php-version }} tools: composer - extensions: swoole-4.7.1 + extensions: openswoole-4.8.1 coverage: none - if: ${{ matrix.php-version == '8.1' }} run: composer install --no-interaction --prefer-dist --ignore-platform-req=php @@ -168,7 +168,7 @@ jobs: with: php-version: ${{ matrix.php-version }} tools: composer - extensions: swoole-4.7.1 + extensions: openswoole-4.8.1 coverage: none - if: ${{ matrix.php-version == '8.1' }} run: composer install --no-interaction --prefer-dist --ignore-platform-req=php @@ -196,7 +196,7 @@ jobs: with: php-version: ${{ matrix.php-version }} tools: composer - extensions: swoole-4.7.1, pdo_sqlsrv-5.10.0beta2 + extensions: openswoole-4.8.1, pdo_sqlsrv-5.10.0beta2 coverage: none - if: ${{ matrix.php-version == '8.1' }} run: composer install --no-interaction --prefer-dist --ignore-platform-req=php @@ -222,7 +222,7 @@ jobs: with: php-version: ${{ matrix.php-version }} tools: composer - extensions: swoole-4.7.1 + extensions: openswoole-4.8.1 coverage: pcov ini-values: pcov.directory=module - if: ${{ matrix.php-version == '8.1' }} @@ -257,7 +257,7 @@ jobs: with: php-version: ${{ matrix.php-version }} tools: composer - extensions: swoole-4.7.1 + extensions: openswoole-4.8.1 coverage: pcov ini-values: pcov.directory=module - if: ${{ matrix.php-version == '8.1' }} diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index b45ee370..56a60e5b 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -20,7 +20,7 @@ jobs: with: php-version: ${{ matrix.php-version }} tools: composer - extensions: swoole-4.6.7 + extensions: openswoole-4.8.1 - if: ${{ matrix.swoole == 'yes' }} run: ./build.sh ${GITHUB_REF#refs/tags/v} - if: ${{ matrix.swoole == 'no' }} diff --git a/Dockerfile b/Dockerfile index 91d0361f..c0b803b8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,7 @@ FROM php:8.0.9-alpine3.14 as base ARG SHLINK_VERSION=latest ENV SHLINK_VERSION ${SHLINK_VERSION} -ENV SWOOLE_VERSION 4.7.1 +ENV OPENSWOOLE_VERSION 4.8.1 ENV PDO_SQLSRV_VERSION 5.10.0beta2 ENV MS_ODBC_SQL_VERSION 17.5.2.2 ENV LC_ALL "C" @@ -40,10 +40,10 @@ RUN if [ $(uname -m) == "x86_64" ]; then \ rm msodbcsql17_${MS_ODBC_SQL_VERSION}-1_amd64.apk ; \ fi -# Install swoole +# Install openswoole RUN apk add --no-cache --virtual .phpize-deps ${PHPIZE_DEPS} && \ - pecl install swoole-${SWOOLE_VERSION} && \ - docker-php-ext-enable swoole && \ + pecl install openswoole-${OPENSWOOLE_VERSION} && \ + docker-php-ext-enable openswoole && \ apk del .phpize-deps @@ -65,7 +65,7 @@ LABEL maintainer="Alejandro Celaya " COPY --from=builder /etc/shlink . RUN ln -s /etc/shlink/bin/cli /usr/local/bin/shlink -# Expose default swoole port +# Expose default openswoole port EXPOSE 8080 # Copy config specific for the image diff --git a/README.md b/README.md index ce91829f..4fe5f5cf 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ First, make sure the host where you are going to run shlink fulfills these requi * PHP 8.0 * The next PHP extensions: json, curl, pdo, intl, gd and gmp. - * apcu extension is recommended if you don't plan to use swoole. + * apcu extension is recommended if you don't plan to use swoole or openswoole. * xml extension is required if you want to generate QR codes in svg format. * MySQL, MariaDB, PostgreSQL, Microsoft SQL Server or SQLite. * The web server of your choice with PHP integration (Apache or Nginx recommended). @@ -49,7 +49,7 @@ In order to run Shlink, you will need a built version of the project. There are The easiest way to install shlink is by using one of the pre-bundled distributable packages. - Go to the [latest version](https://github.com/shlinkio/shlink/releases/latest) and download the `shlink*_dist.zip` file that suits your needs. You will find one for every supported PHP version and with/without swoole integration. + Go to the [latest version](https://github.com/shlinkio/shlink/releases/latest) and download the `shlink*_dist.zip` file that suits your needs. You will find one for every supported PHP version and with/without swoole/openswoole integration. Finally, decompress the file in the location of your choice. diff --git a/composer.json b/composer.json index 94d2536f..880f1d50 100644 --- a/composer.json +++ b/composer.json @@ -47,11 +47,11 @@ "pugx/shortid-php": "^0.7", "ramsey/uuid": "^3.9", "rlanvin/php-ip": "3.0.0-rc2", - "shlinkio/shlink-common": "^4.1", - "shlinkio/shlink-config": "^1.3.1", - "shlinkio/shlink-event-dispatcher": "^2.2", + "shlinkio/shlink-common": "dev-main#2f3ac05 as 4.2", + "shlinkio/shlink-config": "^1.4", + "shlinkio/shlink-event-dispatcher": "dev-main#3925299 as 2.3", "shlinkio/shlink-importer": "^2.4", - "shlinkio/shlink-installer": "^6.2.1", + "shlinkio/shlink-installer": "dev-develop#e3f2e64 as 6.3", "shlinkio/shlink-ip-geolocation": "^2.2", "symfony/console": "^5.4", "symfony/filesystem": "^5.4", diff --git a/config/autoload/logger.local.php.dist b/config/autoload/logger.local.php.dist index 4aa46c68..1da0384b 100644 --- a/config/autoload/logger.local.php.dist +++ b/config/autoload/logger.local.php.dist @@ -5,7 +5,7 @@ declare(strict_types=1); use Monolog\Handler\StreamHandler; use Monolog\Logger; -$isSwoole = extension_loaded('swoole'); +$isSwoole = extension_loaded('openswoole'); // For swoole, send logs to standard output $handler = $isSwoole diff --git a/config/autoload/url-shortener.local.php.dist b/config/autoload/url-shortener.local.php.dist index f34245fb..20140a9b 100644 --- a/config/autoload/url-shortener.local.php.dist +++ b/config/autoload/url-shortener.local.php.dist @@ -2,7 +2,7 @@ declare(strict_types=1); -$isSwoole = extension_loaded('swoole'); +$isSwoole = extension_loaded('openswoole'); return [ diff --git a/data/infra/swoole.Dockerfile b/data/infra/swoole.Dockerfile index 0ceb2030..9cae5e73 100644 --- a/data/infra/swoole.Dockerfile +++ b/data/infra/swoole.Dockerfile @@ -3,7 +3,7 @@ MAINTAINER Alejandro Celaya ENV APCU_VERSION 5.1.20 ENV INOTIFY_VERSION 3.0.0 -ENV SWOOLE_VERSION 4.7.1 +ENV OPENSWOOLE_VERSION 4.8.1 ENV PDO_SQLSRV_VERSION 5.10.0beta2 ENV MS_ODBC_SQL_VERSION 17.5.2.2 @@ -54,12 +54,12 @@ RUN mkdir -p /usr/src/php/ext/inotify \ && docker-php-ext-install inotify \ && rm /tmp/inotify.tar.gz -# Install swoole, pcov and mssql driver +# Install openswoole, pcov and mssql 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 swoole-${SWOOLE_VERSION} pdo_sqlsrv-${PDO_SQLSRV_VERSION} pcov && \ - docker-php-ext-enable swoole pdo_sqlsrv pcov && \ + pecl install openswoole-${OPENSWOOLE_VERSION} pdo_sqlsrv-${PDO_SQLSRV_VERSION} pcov && \ + docker-php-ext-enable openswoole pdo_sqlsrv pcov && \ apk del .phpize-deps && \ rm msodbcsql17_${MS_ODBC_SQL_VERSION}-1_amd64.apk @@ -72,12 +72,12 @@ RUN chmod 777 /home VOLUME /home/shlink WORKDIR /home/shlink -# Expose swoole port +# Expose openswoole port EXPOSE 8080 CMD \ # Install dependencies if the vendor dir does not exist if [[ ! -d "./vendor" ]]; then /usr/local/bin/composer install ; fi && \ - # When restarting the container, swoole might think it is already in execution + # 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 diff --git a/docker/README.md b/docker/README.md index 9f97642c..7c9d3957 100644 --- a/docker/README.md +++ b/docker/README.md @@ -5,7 +5,7 @@ This image provides an easy way to set up [shlink](https://shlink.io) on a container-based runtime. -It exposes a shlink instance served with [swoole](https://www.swoole.co.uk/), which can be linked to external databases to persist data. +It exposes a shlink instance served with [openswoole](https://www.swoole.co.uk/), which can be linked to external databases to persist data. ## Usage diff --git a/docker/docker-entrypoint.sh b/docker/docker-entrypoint.sh index 8847b757..dff3ae98 100644 --- a/docker/docker-entrypoint.sh +++ b/docker/docker-entrypoint.sh @@ -30,6 +30,6 @@ if [ $ENABLE_PERIODIC_VISIT_LOCATE ]; then /usr/sbin/crond & fi -# When restarting the container, swoole might think it is already in execution +# 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 From b855ea92a9455aaadf6ade9fe675812dd28b30b1 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 5 Dec 2021 10:09:06 +0100 Subject: [PATCH 12/66] Updated changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 57d95a4a..7715ac7b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this ## [Unreleased] ### Added -* *Nothing* +* [#1204](https://github.com/shlinkio/shlink/issues/1204) Added support for `openswoole` and migrated official docker image to `openswoole`. ### Changed * [#1218](https://github.com/shlinkio/shlink/issues/1218) Updated to symfony/mercure 0.6. From e519aaaf1ebd27300a516a54fde994e4371078c1 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 5 Dec 2021 15:16:41 +0100 Subject: [PATCH 13/66] Added support to import from YOURLS --- CHANGELOG.md | 1 + composer.json | 2 +- docker-compose.yml | 4 ++++ 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7715ac7b..c9047623 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this ## [Unreleased] ### Added * [#1204](https://github.com/shlinkio/shlink/issues/1204) Added support for `openswoole` and migrated official docker image to `openswoole`. +* [#1242](https://github.com/shlinkio/shlink/issues/1242) Added support to import urls and visits from YOURLS. ### Changed * [#1218](https://github.com/shlinkio/shlink/issues/1218) Updated to symfony/mercure 0.6. diff --git a/composer.json b/composer.json index 880f1d50..e985843e 100644 --- a/composer.json +++ b/composer.json @@ -50,7 +50,7 @@ "shlinkio/shlink-common": "dev-main#2f3ac05 as 4.2", "shlinkio/shlink-config": "^1.4", "shlinkio/shlink-event-dispatcher": "dev-main#3925299 as 2.3", - "shlinkio/shlink-importer": "^2.4", + "shlinkio/shlink-importer": "dev-main#d099072 as 2.5", "shlinkio/shlink-installer": "dev-develop#e3f2e64 as 6.3", "shlinkio/shlink-ip-geolocation": "^2.2", "symfony/console": "^5.4", diff --git a/docker-compose.yml b/docker-compose.yml index 42b0f3a0..8ea38d98 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -31,6 +31,8 @@ services: - shlink_mercure_proxy environment: LC_ALL: C + extra_hosts: + - 'host.docker.internal:host-gateway' shlink_swoole_proxy: container_name: shlink_swoole_proxy @@ -64,6 +66,8 @@ services: - shlink_mercure_proxy environment: LC_ALL: C + extra_hosts: + - 'host.docker.internal:host-gateway' shlink_db: container_name: shlink_db From bf09990f9c1e54a3a90bf814d998a2625a121157 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Mon, 6 Dec 2021 17:10:10 +0100 Subject: [PATCH 14/66] Added support to disable rounding on block size for QR codes --- composer.json | 2 +- config/autoload/qr-codes.global.php | 2 ++ config/constants.php | 1 + module/Core/src/Action/Model/QrCodeParams.php | 16 ++++++++++++++++ module/Core/src/Action/QrCodeAction.php | 3 ++- module/Core/src/Options/QrCodeOptions.php | 12 ++++++++++++ 6 files changed, 34 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index e985843e..b1badf18 100644 --- a/composer.json +++ b/composer.json @@ -47,7 +47,7 @@ "pugx/shortid-php": "^0.7", "ramsey/uuid": "^3.9", "rlanvin/php-ip": "3.0.0-rc2", - "shlinkio/shlink-common": "dev-main#2f3ac05 as 4.2", + "shlinkio/shlink-common": "dev-main#7cc36a6 as 4.2", "shlinkio/shlink-config": "^1.4", "shlinkio/shlink-event-dispatcher": "dev-main#3925299 as 2.3", "shlinkio/shlink-importer": "dev-main#d099072 as 2.5", diff --git a/config/autoload/qr-codes.global.php b/config/autoload/qr-codes.global.php index 1cf6fecb..5f528620 100644 --- a/config/autoload/qr-codes.global.php +++ b/config/autoload/qr-codes.global.php @@ -7,6 +7,7 @@ use function Shlinkio\Shlink\Common\env; 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; return [ @@ -16,6 +17,7 @@ return [ 'margin' => (int) env('DEFAULT_QR_CODE_MARGIN', DEFAULT_QR_CODE_MARGIN), 'format' => env('DEFAULT_QR_CODE_FORMAT', DEFAULT_QR_CODE_FORMAT), 'error_correction' => env('DEFAULT_QR_CODE_ERROR_CORRECTION', DEFAULT_QR_CODE_ERROR_CORRECTION), + 'round_block_size' => (bool) env('DEFAULT_QR_CODE_ROUND_BLOCK_SIZE', DEFAULT_QR_CODE_ROUND_BLOCK_SIZE), ], ]; diff --git a/config/constants.php b/config/constants.php index 6c7aa09e..8171cd66 100644 --- a/config/constants.php +++ b/config/constants.php @@ -18,4 +18,5 @@ const DEFAULT_QR_CODE_SIZE = 300; const DEFAULT_QR_CODE_MARGIN = 0; const DEFAULT_QR_CODE_FORMAT = 'png'; const DEFAULT_QR_CODE_ERROR_CORRECTION = 'l'; +const DEFAULT_QR_CODE_ROUND_BLOCK_SIZE = true; const MIN_TASK_WORKERS = 4; diff --git a/module/Core/src/Action/Model/QrCodeParams.php b/module/Core/src/Action/Model/QrCodeParams.php index 0e889c32..47fb82d4 100644 --- a/module/Core/src/Action/Model/QrCodeParams.php +++ b/module/Core/src/Action/Model/QrCodeParams.php @@ -9,6 +9,9 @@ use Endroid\QrCode\ErrorCorrectionLevel\ErrorCorrectionLevelInterface; use Endroid\QrCode\ErrorCorrectionLevel\ErrorCorrectionLevelLow; use Endroid\QrCode\ErrorCorrectionLevel\ErrorCorrectionLevelMedium; use Endroid\QrCode\ErrorCorrectionLevel\ErrorCorrectionLevelQuartile; +use Endroid\QrCode\RoundBlockSizeMode\RoundBlockSizeModeInterface; +use Endroid\QrCode\RoundBlockSizeMode\RoundBlockSizeModeMargin; +use Endroid\QrCode\RoundBlockSizeMode\RoundBlockSizeModeNone; use Endroid\QrCode\Writer\PngWriter; use Endroid\QrCode\Writer\SvgWriter; use Endroid\QrCode\Writer\WriterInterface; @@ -31,6 +34,7 @@ final class QrCodeParams private int $margin, private WriterInterface $writer, private ErrorCorrectionLevelInterface $errorCorrectionLevel, + private RoundBlockSizeModeInterface $roundBlockSizeMode, ) { } @@ -43,6 +47,7 @@ final class QrCodeParams self::resolveMargin($query, $defaults), self::resolveWriter($query, $defaults), self::resolveErrorCorrection($query, $defaults), + self::resolveRoundBlockSize($query, $defaults), ); } @@ -90,6 +95,12 @@ final class QrCodeParams }; } + private static function resolveRoundBlockSize(array $query, QrCodeOptions $defaults): RoundBlockSizeModeInterface + { + $doNotRoundBlockSize = ($query['roundBlockSize'] ?? null) === 'false' || ! $defaults->roundBlockSize(); + return $doNotRoundBlockSize ? new RoundBlockSizeModeNone() : new RoundBlockSizeModeMargin(); + } + private static function normalizeParam(string $param): string { return strtolower(trim($param)); @@ -114,4 +125,9 @@ final class QrCodeParams { return $this->errorCorrectionLevel; } + + public function roundBlockSizeMode(): RoundBlockSizeModeInterface + { + return $this->roundBlockSizeMode; + } } diff --git a/module/Core/src/Action/QrCodeAction.php b/module/Core/src/Action/QrCodeAction.php index f8d2e275..7772a5c8 100644 --- a/module/Core/src/Action/QrCodeAction.php +++ b/module/Core/src/Action/QrCodeAction.php @@ -45,7 +45,8 @@ class QrCodeAction implements MiddlewareInterface ->size($params->size()) ->margin($params->margin()) ->writer($params->writer()) - ->errorCorrectionLevel($params->errorCorrectionLevel()); + ->errorCorrectionLevel($params->errorCorrectionLevel()) + ->roundBlockSizeMode($params->roundBlockSizeMode()); return new QrCodeResponse($qrCodeBuilder->build()); } diff --git a/module/Core/src/Options/QrCodeOptions.php b/module/Core/src/Options/QrCodeOptions.php index 80d6e456..3dfc9a53 100644 --- a/module/Core/src/Options/QrCodeOptions.php +++ b/module/Core/src/Options/QrCodeOptions.php @@ -9,6 +9,7 @@ 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 @@ -17,6 +18,7 @@ class QrCodeOptions extends AbstractOptions 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 { @@ -57,4 +59,14 @@ class QrCodeOptions extends AbstractOptions { $this->errorCorrection = $errorCorrection; } + + public function roundBlockSize(): bool + { + return $this->roundBlockSize; + } + + protected function setRoundBlockSize(bool $roundBlockSize): void + { + $this->roundBlockSize = $roundBlockSize; + } } From bdc89e20564ed5da47f7ae0c7e5cded0f02018b2 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Mon, 6 Dec 2021 17:15:19 +0100 Subject: [PATCH 15/66] Fixed execution on non-swoole contexts --- config/config.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/config/config.php b/config/config.php index 887aa365..ccb61cbb 100644 --- a/config/config.php +++ b/config/config.php @@ -13,11 +13,17 @@ use Mezzio\Swoole; use function class_exists; use function Shlinkio\Shlink\Common\env; +use const PHP_SAPI; + +$isCli = PHP_SAPI === 'cli'; + return (new ConfigAggregator\ConfigAggregator([ Mezzio\ConfigProvider::class, Mezzio\Router\ConfigProvider::class, Mezzio\Router\FastRouteRouter\ConfigProvider::class, - class_exists(Swoole\ConfigProvider::class) ? Swoole\ConfigProvider::class : new ConfigAggregator\ArrayProvider([]), + $isCli && class_exists(Swoole\ConfigProvider::class) + ? Swoole\ConfigProvider::class + : new ConfigAggregator\ArrayProvider([]), ProblemDetails\ConfigProvider::class, Diactoros\ConfigProvider::class, Common\ConfigProvider::class, From 1a75bd87d82bc1b31bef829db9b3d7f9ffffd648 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Mon, 6 Dec 2021 17:35:32 +0100 Subject: [PATCH 16/66] Updated installer with support for QR code block size rounding --- CHANGELOG.md | 1 + composer.json | 2 +- config/autoload/installer.global.php | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c9047623..b7f49abe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this ### Added * [#1204](https://github.com/shlinkio/shlink/issues/1204) Added support for `openswoole` and migrated official docker image to `openswoole`. * [#1242](https://github.com/shlinkio/shlink/issues/1242) Added support to import urls and visits from YOURLS. +* [#1235](https://github.com/shlinkio/shlink/issues/1235) Added support to disable rounding QR codes block sizing via config option, env var or query param. ### Changed * [#1218](https://github.com/shlinkio/shlink/issues/1218) Updated to symfony/mercure 0.6. diff --git a/composer.json b/composer.json index b1badf18..09aaf729 100644 --- a/composer.json +++ b/composer.json @@ -51,7 +51,7 @@ "shlinkio/shlink-config": "^1.4", "shlinkio/shlink-event-dispatcher": "dev-main#3925299 as 2.3", "shlinkio/shlink-importer": "dev-main#d099072 as 2.5", - "shlinkio/shlink-installer": "dev-develop#e3f2e64 as 6.3", + "shlinkio/shlink-installer": "dev-develop#7dd00fb as 6.3", "shlinkio/shlink-ip-geolocation": "^2.2", "symfony/console": "^5.4", "symfony/filesystem": "^5.4", diff --git a/config/autoload/installer.global.php b/config/autoload/installer.global.php index 24461e70..def478af 100644 --- a/config/autoload/installer.global.php +++ b/config/autoload/installer.global.php @@ -56,6 +56,7 @@ return [ Option\QrCode\DefaultMarginConfigOption::class, Option\QrCode\DefaultFormatConfigOption::class, Option\QrCode\DefaultErrorCorrectionConfigOption::class, + Option\QrCode\DefaultRoundBlockSizeConfigOption::class, ], 'installation_commands' => [ From 813ae71aad2e76fce27e5cecdf0915fa95f82909 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Mon, 6 Dec 2021 18:06:29 +0100 Subject: [PATCH 17/66] Added test checking if auto margin is added to QR codes --- module/Core/src/Action/Model/QrCodeParams.php | 4 +- module/Core/test/Action/QrCodeActionTest.php | 44 ++++++++++++++++++- 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/module/Core/src/Action/Model/QrCodeParams.php b/module/Core/src/Action/Model/QrCodeParams.php index 47fb82d4..03643e4c 100644 --- a/module/Core/src/Action/Model/QrCodeParams.php +++ b/module/Core/src/Action/Model/QrCodeParams.php @@ -97,7 +97,9 @@ final class QrCodeParams private static function resolveRoundBlockSize(array $query, QrCodeOptions $defaults): RoundBlockSizeModeInterface { - $doNotRoundBlockSize = ($query['roundBlockSize'] ?? null) === 'false' || ! $defaults->roundBlockSize(); + $doNotRoundBlockSize = isset($query['roundBlockSize']) + ? $query['roundBlockSize'] === 'false' + : ! $defaults->roundBlockSize(); return $doNotRoundBlockSize ? new RoundBlockSizeModeNone() : new RoundBlockSizeModeMargin(); } diff --git a/module/Core/test/Action/QrCodeActionTest.php b/module/Core/test/Action/QrCodeActionTest.php index 1fdc35ef..664a51a2 100644 --- a/module/Core/test/Action/QrCodeActionTest.php +++ b/module/Core/test/Action/QrCodeActionTest.php @@ -25,11 +25,16 @@ use Shlinkio\Shlink\Core\Service\ShortUrl\ShortUrlResolverInterface; use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifier; use function getimagesizefromstring; +use function imagecolorat; +use function imagecreatefromstring; class QrCodeActionTest extends TestCase { use ProphecyTrait; + private const WHITE = 0xFFFFFF; + private const BLACK = 0x0; + private QrCodeAction $action; private ObjectProphecy $urlResolver; private QrCodeOptions $options; @@ -135,7 +140,7 @@ class QrCodeActionTest extends TestCase $delegate = $this->prophesize(RequestHandlerInterface::class); $resp = $this->action->process($req->withAttribute('shortCode', $code), $delegate->reveal()); - [$size] = getimagesizefromstring((string) $resp->getBody()); + [$size] = getimagesizefromstring($resp->getBody()->__toString()); self::assertEquals($expectedSize, $size); } @@ -199,4 +204,41 @@ class QrCodeActionTest extends TestCase 538, ]; } + + /** + * @test + * @dataProvider provideRoundBlockSize + */ + public function imageCanRemoveExtraMarginWhenBlockRoundIsDisabled( + array $defaults, + ?string $roundBlockSize, + int $expectedColor, + ): void { + $this->options->setFromArray($defaults); + $code = 'abc123'; + $req = ServerRequestFactory::fromGlobals() + ->withQueryParams(['size' => 250, 'roundBlockSize' => $roundBlockSize]) + ->withAttribute('shortCode', $code); + + $this->urlResolver->resolveEnabledShortUrl(new ShortUrlIdentifier($code, ''))->willReturn( + ShortUrl::withLongUrl('https://shlink.io'), + ); + $delegate = $this->prophesize(RequestHandlerInterface::class); + + $resp = $this->action->process($req, $delegate->reveal()); + $image = imagecreatefromstring($resp->getBody()->__toString()); + $color = imagecolorat($image, 1, 1); + + self::assertEquals($color, $expectedColor); + } + + 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]; + } } From cc7ded1be76e8992f09b197c627ad1c9fb4ab9d5 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Tue, 7 Dec 2021 09:55:06 +0100 Subject: [PATCH 18/66] Removed allowed failures in CI pipeline for PHP 8.1 --- .github/workflows/ci.yml | 48 +++++++--------------------------------- CHANGELOG.md | 6 +++++ 2 files changed, 14 insertions(+), 40 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 94ebcc9e..1db3f3eb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,7 +49,6 @@ jobs: strategy: matrix: php-version: ['8.0', '8.1'] - continue-on-error: ${{ matrix.php-version == '8.1' }} steps: - name: Checkout code uses: actions/checkout@v2 @@ -61,10 +60,7 @@ jobs: extensions: openswoole-4.8.1 coverage: pcov ini-values: pcov.directory=module - - if: ${{ matrix.php-version == '8.1' }} - run: composer install --no-interaction --prefer-dist --ignore-platform-req=php - - if: ${{ matrix.php-version != '8.1' }} - run: composer install --no-interaction --prefer-dist + - run: composer install --no-interaction --prefer-dist - run: composer test:unit:ci - uses: actions/upload-artifact@v2 if: ${{ matrix.php-version == '8.0' }} @@ -79,7 +75,6 @@ jobs: strategy: matrix: php-version: ['8.0', '8.1'] - continue-on-error: ${{ matrix.php-version == '8.1' }} steps: - name: Checkout code uses: actions/checkout@v2 @@ -91,10 +86,7 @@ jobs: extensions: openswoole-4.8.1 coverage: pcov ini-values: pcov.directory=module - - if: ${{ matrix.php-version == '8.1' }} - run: composer install --no-interaction --prefer-dist --ignore-platform-req=php - - if: ${{ matrix.php-version != '8.1' }} - run: composer install --no-interaction --prefer-dist + - run: composer install --no-interaction --prefer-dist - run: composer test:db:sqlite:ci - uses: actions/upload-artifact@v2 if: ${{ matrix.php-version == '8.0' }} @@ -109,7 +101,6 @@ jobs: strategy: matrix: php-version: ['8.0', '8.1'] - continue-on-error: ${{ matrix.php-version == '8.1' }} steps: - name: Checkout code uses: actions/checkout@v2 @@ -122,10 +113,7 @@ jobs: tools: composer extensions: openswoole-4.8.1 coverage: none - - if: ${{ matrix.php-version == '8.1' }} - run: composer install --no-interaction --prefer-dist --ignore-platform-req=php - - if: ${{ matrix.php-version != '8.1' }} - run: composer install --no-interaction --prefer-dist + - run: composer install --no-interaction --prefer-dist - run: composer test:db:mysql db-tests-maria: @@ -133,7 +121,6 @@ jobs: strategy: matrix: php-version: ['8.0', '8.1'] - continue-on-error: ${{ matrix.php-version == '8.1' }} steps: - name: Checkout code uses: actions/checkout@v2 @@ -146,10 +133,7 @@ jobs: tools: composer extensions: openswoole-4.8.1 coverage: none - - if: ${{ matrix.php-version == '8.1' }} - run: composer install --no-interaction --prefer-dist --ignore-platform-req=php - - if: ${{ matrix.php-version != '8.1' }} - run: composer install --no-interaction --prefer-dist + - run: composer install --no-interaction --prefer-dist - run: composer test:db:maria db-tests-postgres: @@ -157,7 +141,6 @@ jobs: strategy: matrix: php-version: ['8.0', '8.1'] - continue-on-error: ${{ matrix.php-version == '8.1' }} steps: - name: Checkout code uses: actions/checkout@v2 @@ -170,10 +153,7 @@ jobs: tools: composer extensions: openswoole-4.8.1 coverage: none - - if: ${{ matrix.php-version == '8.1' }} - run: composer install --no-interaction --prefer-dist --ignore-platform-req=php - - if: ${{ matrix.php-version != '8.1' }} - run: composer install --no-interaction --prefer-dist + - run: composer install --no-interaction --prefer-dist - run: composer test:db:postgres db-tests-ms: @@ -181,7 +161,6 @@ jobs: strategy: matrix: php-version: ['8.0', '8.1'] - continue-on-error: ${{ matrix.php-version == '8.1' }} env: LC_ALL: C steps: @@ -198,10 +177,7 @@ jobs: tools: composer extensions: openswoole-4.8.1, pdo_sqlsrv-5.10.0beta2 coverage: none - - if: ${{ matrix.php-version == '8.1' }} - run: composer install --no-interaction --prefer-dist --ignore-platform-req=php - - if: ${{ matrix.php-version != '8.1' }} - run: composer install --no-interaction --prefer-dist + - run: composer install --no-interaction --prefer-dist - name: Create test database run: docker-compose exec -T shlink_db_ms /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P 'Passw0rd!' -Q "CREATE DATABASE shlink_test;" - run: composer test:db:ms @@ -211,7 +187,6 @@ jobs: strategy: matrix: php-version: ['8.0', '8.1'] - continue-on-error: ${{ matrix.php-version == '8.1' }} steps: - name: Checkout code uses: actions/checkout@v2 @@ -225,10 +200,7 @@ jobs: extensions: openswoole-4.8.1 coverage: pcov ini-values: pcov.directory=module - - if: ${{ matrix.php-version == '8.1' }} - run: composer install --no-interaction --prefer-dist --ignore-platform-req=php - - if: ${{ matrix.php-version != '8.1' }} - run: composer install --no-interaction --prefer-dist + - run: composer install --no-interaction --prefer-dist - run: bin/test/run-api-tests.sh - uses: actions/upload-artifact@v2 if: ${{ matrix.php-version == '8.0' }} @@ -248,7 +220,6 @@ jobs: matrix: php-version: ['8.0', '8.1'] test-group: ['unit', 'db'] - continue-on-error: ${{ matrix.php-version == '8.1' }} steps: - name: Checkout code uses: actions/checkout@v2 @@ -260,10 +231,7 @@ jobs: extensions: openswoole-4.8.1 coverage: pcov ini-values: pcov.directory=module - - if: ${{ matrix.php-version == '8.1' }} - run: composer install --no-interaction --prefer-dist --ignore-platform-req=php - - if: ${{ matrix.php-version != '8.1' }} - run: composer install --no-interaction --prefer-dist + - run: composer install --no-interaction --prefer-dist - uses: actions/download-artifact@v2 with: path: build diff --git a/CHANGELOG.md b/CHANGELOG.md index b7f49abe..c27aabcb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this ### Added * [#1204](https://github.com/shlinkio/shlink/issues/1204) Added support for `openswoole` and migrated official docker image to `openswoole`. * [#1242](https://github.com/shlinkio/shlink/issues/1242) Added support to import urls and visits from YOURLS. + + In order to do it, you need to first install this [dedicated plugin](https://slnk.to/yourls-import) in YOURLS, and then run the `short-url:import yourls` command, as with any other source. + * [#1235](https://github.com/shlinkio/shlink/issues/1235) Added support to disable rounding QR codes block sizing via config option, env var or query param. +* [#1188](https://github.com/shlinkio/shlink/issues/1188) Added support for PHP 8.1. + + The official docker image has also been updated to use PHP 8.1 by default. ### Changed * [#1218](https://github.com/shlinkio/shlink/issues/1218) Updated to symfony/mercure 0.6. From bb87bdce8a585d7541b1fb09f2a9dd5eb12c78ba Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Tue, 7 Dec 2021 10:43:36 +0100 Subject: [PATCH 19/66] Updated docker images to use PHP 8.1 --- Dockerfile | 2 +- composer.json | 2 +- data/infra/php.Dockerfile | 4 ++-- data/infra/swoole.Dockerfile | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Dockerfile b/Dockerfile index c0b803b8..d26c7848 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM php:8.0.9-alpine3.14 as base +FROM php:8.1.0-alpine3.15 as base ARG SHLINK_VERSION=latest ENV SHLINK_VERSION ${SHLINK_VERSION} diff --git a/composer.json b/composer.json index 09aaf729..57b54946 100644 --- a/composer.json +++ b/composer.json @@ -45,7 +45,7 @@ "php-middleware/request-id": "^4.1", "predis/predis": "^1.1", "pugx/shortid-php": "^0.7", - "ramsey/uuid": "^3.9", + "ramsey/uuid": "^4.2", "rlanvin/php-ip": "3.0.0-rc2", "shlinkio/shlink-common": "dev-main#7cc36a6 as 4.2", "shlinkio/shlink-config": "^1.4", diff --git a/data/infra/php.Dockerfile b/data/infra/php.Dockerfile index fe6c2722..86f95361 100644 --- a/data/infra/php.Dockerfile +++ b/data/infra/php.Dockerfile @@ -1,7 +1,7 @@ -FROM php:8.0.9-fpm-alpine3.14 +FROM php:8.1.0-fpm-alpine3.15 MAINTAINER Alejandro Celaya -ENV APCU_VERSION 5.1.20 +ENV APCU_VERSION 5.1.21 ENV PDO_SQLSRV_VERSION 5.10.0beta2 ENV MS_ODBC_SQL_VERSION 17.5.2.2 diff --git a/data/infra/swoole.Dockerfile b/data/infra/swoole.Dockerfile index 9cae5e73..74b83d07 100644 --- a/data/infra/swoole.Dockerfile +++ b/data/infra/swoole.Dockerfile @@ -1,7 +1,7 @@ -FROM php:8.0.9-alpine3.14 +FROM php:8.1.0-alpine3.15 MAINTAINER Alejandro Celaya -ENV APCU_VERSION 5.1.20 +ENV APCU_VERSION 5.1.21 ENV INOTIFY_VERSION 3.0.0 ENV OPENSWOOLE_VERSION 4.8.1 ENV PDO_SQLSRV_VERSION 5.10.0beta2 From 13d70cd12a2ef8a28f0d1a5003252d5d86fb0665 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Tue, 7 Dec 2021 19:14:56 +0100 Subject: [PATCH 20/66] Updated docker entry point to make sure debugging and verbosity of commands works as expected --- CHANGELOG.md | 2 +- docker/docker-entrypoint.sh | 13 ++++++++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b7f49abe..b5729915 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,7 +22,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this * *Nothing* ### Fixed -* *Nothing* +* [#1206](https://github.com/shlinkio/shlink/issues/1206) Fixed debugging of the docker image, so that it does not run the commands with `-q` when the `SHELL_VERBOSITY` env var has been provided. ## [2.9.3] - 2021-11-15 diff --git a/docker/docker-entrypoint.sh b/docker/docker-entrypoint.sh index dff3ae98..8f48e20a 100644 --- a/docker/docker-entrypoint.sh +++ b/docker/docker-entrypoint.sh @@ -1,24 +1,27 @@ #!/usr/bin/env sh set -e +# If SHELL_VERBOSITY was not explicitly provided, run commands in quite mode (-q) +[ $SHELL_VERBOSITY ] && flags="" || flags="-q" + cd /etc/shlink echo "Creating fresh database if needed..." -php bin/cli db:create -n -q +php bin/cli db:create -n ${flags} echo "Updating database..." -php bin/cli db:migrate -n -q +php bin/cli db:migrate -n ${flags} echo "Generating proxies..." -php vendor/doctrine/orm/bin/doctrine.php orm:generate-proxies -n -q +php vendor/doctrine/orm/bin/doctrine.php orm:generate-proxies -n ${flags} echo "Clearing entities cache..." -php vendor/doctrine/orm/bin/doctrine.php orm:clear-cache:metadata -n -q +php vendor/doctrine/orm/bin/doctrine.php 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 echo "Downloading GeoLite2 db file..." - php bin/cli visit:download-db -n -q + php bin/cli visit:download-db -n ${flags} fi # Periodicaly run visit:locate every hour From 5e722c830f231176de9484ae04ea65de0f06dfe5 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Tue, 7 Dec 2021 21:13:47 +0100 Subject: [PATCH 21/66] Allowed to set redirects for default domain via command line or API --- docs/swagger/paths/v2_domains_redirects.json | 10 ------ module/Core/src/Domain/DomainService.php | 29 ++++++++++------ .../src/Domain/DomainServiceInterface.php | 2 -- .../src/Exception/InvalidDomainException.php | 33 ------------------- module/Core/test/Domain/DomainServiceTest.php | 14 +------- .../Exception/InvalidDomainExceptionTest.php | 24 -------------- 6 files changed, 20 insertions(+), 92 deletions(-) delete mode 100644 module/Core/src/Exception/InvalidDomainException.php delete mode 100644 module/Core/test/Exception/InvalidDomainExceptionTest.php diff --git a/docs/swagger/paths/v2_domains_redirects.json b/docs/swagger/paths/v2_domains_redirects.json index d9863dcd..031e1d43 100644 --- a/docs/swagger/paths/v2_domains_redirects.json +++ b/docs/swagger/paths/v2_domains_redirects.json @@ -99,16 +99,6 @@ } } }, - "403": { - "description": "Default domain was provided, and it cannot be edited this way.", - "content": { - "application/problem+json": { - "schema": { - "$ref": "../definitions/Error.json" - } - } - } - }, "500": { "description": "Unexpected error.", "content": { diff --git a/module/Core/src/Domain/DomainService.php b/module/Core/src/Domain/DomainService.php index b5141324..b1ba194f 100644 --- a/module/Core/src/Domain/DomainService.php +++ b/module/Core/src/Domain/DomainService.php @@ -10,11 +10,12 @@ use Shlinkio\Shlink\Core\Domain\Model\DomainItem; use Shlinkio\Shlink\Core\Domain\Repository\DomainRepositoryInterface; use Shlinkio\Shlink\Core\Entity\Domain; use Shlinkio\Shlink\Core\Exception\DomainNotFoundException; -use Shlinkio\Shlink\Core\Exception\InvalidDomainException; use Shlinkio\Shlink\Core\Options\NotFoundRedirectOptions; use Shlinkio\Shlink\Rest\ApiKey\Role; use Shlinkio\Shlink\Rest\Entity\ApiKey; +use function Functional\first; +use function Functional\group; use function Functional\map; class DomainService implements DomainServiceInterface @@ -31,9 +32,7 @@ class DomainService implements DomainServiceInterface */ public function listDomains(?ApiKey $apiKey = null): array { - /** @var DomainRepositoryInterface $repo */ - $repo = $this->em->getRepository(Domain::class); - $domains = $repo->findDomainsWithout($this->defaultDomain, $apiKey); + [$default, $domains] = $this->defaultDomainAndRest($apiKey); $mappedDomains = map($domains, fn (Domain $domain) => DomainItem::forExistingDomain($domain)); if ($apiKey?->hasRole(Role::DOMAIN_SPECIFIC)) { @@ -41,11 +40,26 @@ class DomainService implements DomainServiceInterface } return [ - DomainItem::forDefaultDomain($this->defaultDomain, $this->redirectOptions), + DomainItem::forDefaultDomain($this->defaultDomain, $default ?? $this->redirectOptions), ...$mappedDomains, ]; } + /** + * @return array{Domain|null, Domain[]} + */ + private function defaultDomainAndRest(?ApiKey $apiKey): array + { + /** @var DomainRepositoryInterface $repo */ + $repo = $this->em->getRepository(Domain::class); + $groups = group( + $repo->findDomainsWithout(null, $apiKey), // FIXME Always called with null as first arg + fn (Domain $domain) => $domain->getAuthority() === $this->defaultDomain ? 'default' : 'domains', + ); + + return [first($groups['default'] ?? []), $groups['domains'] ?? []]; + } + /** * @throws DomainNotFoundException */ @@ -79,17 +93,12 @@ class DomainService implements DomainServiceInterface /** * @throws DomainNotFoundException - * @throws InvalidDomainException */ public function configureNotFoundRedirects( string $authority, NotFoundRedirects $notFoundRedirects, ?ApiKey $apiKey = null, ): Domain { - if ($authority === $this->defaultDomain) { - throw InvalidDomainException::forDefaultDomainRedirects(); - } - $domain = $this->getPersistedDomain($authority, $apiKey); $domain->configureNotFoundRedirects($notFoundRedirects); diff --git a/module/Core/src/Domain/DomainServiceInterface.php b/module/Core/src/Domain/DomainServiceInterface.php index 7748284d..9ac48e69 100644 --- a/module/Core/src/Domain/DomainServiceInterface.php +++ b/module/Core/src/Domain/DomainServiceInterface.php @@ -8,7 +8,6 @@ use Shlinkio\Shlink\Core\Config\NotFoundRedirects; use Shlinkio\Shlink\Core\Domain\Model\DomainItem; use Shlinkio\Shlink\Core\Entity\Domain; use Shlinkio\Shlink\Core\Exception\DomainNotFoundException; -use Shlinkio\Shlink\Core\Exception\InvalidDomainException; use Shlinkio\Shlink\Rest\Entity\ApiKey; interface DomainServiceInterface @@ -32,7 +31,6 @@ interface DomainServiceInterface /** * @throws DomainNotFoundException If the API key is restricted to one domain and a different one is provided - * @throws InvalidDomainException If default domain is provided */ public function configureNotFoundRedirects( string $authority, diff --git a/module/Core/src/Exception/InvalidDomainException.php b/module/Core/src/Exception/InvalidDomainException.php deleted file mode 100644 index d41e71ac..00000000 --- a/module/Core/src/Exception/InvalidDomainException.php +++ /dev/null @@ -1,33 +0,0 @@ -detail = $e->getMessage(); - $e->title = self::TITLE; - $e->type = self::TYPE; - $e->status = StatusCodeInterface::STATUS_FORBIDDEN; - - return $e; - } -} diff --git a/module/Core/test/Domain/DomainServiceTest.php b/module/Core/test/Domain/DomainServiceTest.php index 159fb6ca..337438b5 100644 --- a/module/Core/test/Domain/DomainServiceTest.php +++ b/module/Core/test/Domain/DomainServiceTest.php @@ -15,7 +15,6 @@ use Shlinkio\Shlink\Core\Domain\Model\DomainItem; use Shlinkio\Shlink\Core\Domain\Repository\DomainRepositoryInterface; use Shlinkio\Shlink\Core\Entity\Domain; use Shlinkio\Shlink\Core\Exception\DomainNotFoundException; -use Shlinkio\Shlink\Core\Exception\InvalidDomainException; use Shlinkio\Shlink\Core\Options\NotFoundRedirectOptions; use Shlinkio\Shlink\Rest\ApiKey\Model\ApiKeyMeta; use Shlinkio\Shlink\Rest\ApiKey\Model\RoleDefinition; @@ -42,7 +41,7 @@ class DomainServiceTest extends TestCase { $repo = $this->prophesize(DomainRepositoryInterface::class); $getRepo = $this->em->getRepository(Domain::class)->willReturn($repo->reveal()); - $findDomains = $repo->findDomainsWithout('default.com', $apiKey)->willReturn($domains); + $findDomains = $repo->findDomainsWithout(null, $apiKey)->willReturn($domains); $result = $this->domainService->listDomains($apiKey); @@ -214,15 +213,4 @@ class DomainServiceTest extends TestCase yield 'domain not found and author API key' => [null, $authorApiKey]; yield 'domain found and author API key' => [$domain, $authorApiKey]; } - - /** @test */ - public function anExceptionIsThrowsWhenTryingToEditRedirectsForDefaultDomain(): void - { - $this->expectException(InvalidDomainException::class); - $this->expectExceptionMessage( - 'You cannot configure default domain\'s redirects this way. Use the configuration or env vars.', - ); - - $this->domainService->configureNotFoundRedirects('default.com', NotFoundRedirects::withoutRedirects()); - } } diff --git a/module/Core/test/Exception/InvalidDomainExceptionTest.php b/module/Core/test/Exception/InvalidDomainExceptionTest.php deleted file mode 100644 index 06b78ff2..00000000 --- a/module/Core/test/Exception/InvalidDomainExceptionTest.php +++ /dev/null @@ -1,24 +0,0 @@ -getMessage()); - self::assertEquals($expected, $e->getDetail()); - self::assertEquals('Invalid domain', $e->getTitle()); - self::assertEquals('INVALID_DOMAIN', $e->getType()); - self::assertEquals(403, $e->getStatus()); - } -} From 3a4550fe24f6e30b6feab8f9ddb7a7745433d6ca Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Wed, 8 Dec 2021 09:40:43 +0100 Subject: [PATCH 22/66] Updated dependencies to corresponding versions supporting PHP 8.1 --- composer.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/composer.json b/composer.json index 57b54946..e647e58c 100644 --- a/composer.json +++ b/composer.json @@ -24,7 +24,7 @@ "geoip2/geoip2": "^2.12", "guzzlehttp/guzzle": "^7.4", "happyr/doctrine-specification": "^2.0", - "jaybizzle/crawler-detect": "^1.2", + "jaybizzle/crawler-detect": "^1.2.110", "laminas/laminas-config": "^3.7", "laminas/laminas-config-aggregator": "^1.7", "laminas/laminas-diactoros": "^2.8", @@ -41,13 +41,13 @@ "monolog/monolog": "^2.3", "nikolaposa/monolog-factory": "^3.1", "ocramius/proxy-manager": "^2.11", - "pagerfanta/core": "^2.7", + "pagerfanta/core": "^3.5", "php-middleware/request-id": "^4.1", "predis/predis": "^1.1", - "pugx/shortid-php": "^0.7", + "pugx/shortid-php": "^1.0", "ramsey/uuid": "^4.2", - "rlanvin/php-ip": "3.0.0-rc2", - "shlinkio/shlink-common": "dev-main#7cc36a6 as 4.2", + "rlanvin/php-ip": "dev-master#6b3a785 as 3.0", + "shlinkio/shlink-common": "dev-main#a6cbcb6 as 4.2", "shlinkio/shlink-config": "^1.4", "shlinkio/shlink-event-dispatcher": "dev-main#3925299 as 2.3", "shlinkio/shlink-importer": "dev-main#d099072 as 2.5", @@ -64,7 +64,7 @@ "devster/ubench": "^2.1", "dms/phpunit-arraysubset-asserts": "^0.3.0", "eaglewu/swoole-ide-helper": "dev-master", - "infection/infection": "^0.25.3", + "infection/infection": "^0.25.4", "phpspec/prophecy-phpunit": "^2.0", "phpstan/phpstan": "^1.2", "phpstan/phpstan-doctrine": "^1.0", From f36140388809bc118711887960c51f70b3a66dd2 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Wed, 8 Dec 2021 17:36:40 +0100 Subject: [PATCH 23/66] Updated paginator types --- composer.json | 2 +- .../CLI/src/Command/ShortUrl/ListShortUrlsCommand.php | 2 +- .../Command/ShortUrl/ListShortUrlsCommandTest.php | 2 +- module/Core/src/Model/VisitsParams.php | 6 +++--- module/Core/src/Repository/VisitRepository.php | 4 +--- .../src/Validation/ShortUrlsParamsInputFilter.php | 3 ++- module/Rest/test-api/Action/OrphanVisitsTest.php | 3 ++- module/Rest/test-api/Action/ShortUrlVisitsTest.php | 11 +++++++++-- phpstan.neon | 3 +++ 9 files changed, 23 insertions(+), 13 deletions(-) diff --git a/composer.json b/composer.json index e647e58c..2926a1af 100644 --- a/composer.json +++ b/composer.json @@ -47,7 +47,7 @@ "pugx/shortid-php": "^1.0", "ramsey/uuid": "^4.2", "rlanvin/php-ip": "dev-master#6b3a785 as 3.0", - "shlinkio/shlink-common": "dev-main#a6cbcb6 as 4.2", + "shlinkio/shlink-common": "dev-main#c2e3442 as 4.2", "shlinkio/shlink-config": "^1.4", "shlinkio/shlink-event-dispatcher": "dev-main#3925299 as 2.3", "shlinkio/shlink-importer": "dev-main#d099072 as 2.5", diff --git a/module/CLI/src/Command/ShortUrl/ListShortUrlsCommand.php b/module/CLI/src/Command/ShortUrl/ListShortUrlsCommand.php index 53e47d3c..cbc6e3ee 100644 --- a/module/CLI/src/Command/ShortUrl/ListShortUrlsCommand.php +++ b/module/CLI/src/Command/ShortUrl/ListShortUrlsCommand.php @@ -131,7 +131,7 @@ class ListShortUrlsCommand extends AbstractWithDateRangeCommand ]; if ($all) { - $data[ShortUrlsParamsInputFilter::ITEMS_PER_PAGE] = -1; + $data[ShortUrlsParamsInputFilter::ITEMS_PER_PAGE] = Paginator::ALL_ITEMS; } do { diff --git a/module/CLI/test/Command/ShortUrl/ListShortUrlsCommandTest.php b/module/CLI/test/Command/ShortUrl/ListShortUrlsCommandTest.php index 8150d0c8..4a974d73 100644 --- a/module/CLI/test/Command/ShortUrl/ListShortUrlsCommandTest.php +++ b/module/CLI/test/Command/ShortUrl/ListShortUrlsCommandTest.php @@ -271,7 +271,7 @@ class ListShortUrlsCommandTest extends TestCase 'startDate' => null, 'endDate' => null, 'orderBy' => null, - 'itemsPerPage' => -1, + 'itemsPerPage' => Paginator::ALL_ITEMS, ]))->willReturn(new Paginator(new ArrayAdapter([]))); $this->commandTester->execute(['--all' => true]); diff --git a/module/Core/src/Model/VisitsParams.php b/module/Core/src/Model/VisitsParams.php index ed98d4d2..dd5a656d 100644 --- a/module/Core/src/Model/VisitsParams.php +++ b/module/Core/src/Model/VisitsParams.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Shlinkio\Shlink\Core\Model; +use Shlinkio\Shlink\Common\Paginator\Paginator; use Shlinkio\Shlink\Common\Util\DateRange; use function Shlinkio\Shlink\Core\parseDateRangeFromQuery; @@ -11,7 +12,6 @@ use function Shlinkio\Shlink\Core\parseDateRangeFromQuery; final class VisitsParams { private const FIRST_PAGE = 1; - private const ALL_ITEMS = -1; private DateRange $dateRange; private int $page; @@ -36,10 +36,10 @@ final class VisitsParams private function determineItemsPerPage(?int $itemsPerPage): int { if ($itemsPerPage !== null && $itemsPerPage < 0) { - return self::ALL_ITEMS; + return Paginator::ALL_ITEMS; } - return $itemsPerPage ?? self::ALL_ITEMS; + return $itemsPerPage ?? Paginator::ALL_ITEMS; } public static function fromRawData(array $query): self diff --git a/module/Core/src/Repository/VisitRepository.php b/module/Core/src/Repository/VisitRepository.php index 0fe539af..5c39c21e 100644 --- a/module/Core/src/Repository/VisitRepository.php +++ b/module/Core/src/Repository/VisitRepository.php @@ -226,8 +226,6 @@ class VisitRepository extends EntitySpecificationRepository implements VisitRepo 'id' => 'visit_location_id', ]); - $query = $this->getEntityManager()->createNativeQuery($nativeQb->getSQL(), $rsm); - - return $query->getResult(); + return $this->getEntityManager()->createNativeQuery($nativeQb->getSQL(), $rsm)->getResult(); } } diff --git a/module/Core/src/Validation/ShortUrlsParamsInputFilter.php b/module/Core/src/Validation/ShortUrlsParamsInputFilter.php index 871995dd..c62845d4 100644 --- a/module/Core/src/Validation/ShortUrlsParamsInputFilter.php +++ b/module/Core/src/Validation/ShortUrlsParamsInputFilter.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace Shlinkio\Shlink\Core\Validation; use Laminas\InputFilter\InputFilter; +use Shlinkio\Shlink\Common\Paginator\Paginator; use Shlinkio\Shlink\Common\Validation; class ShortUrlsParamsInputFilter extends InputFilter @@ -32,7 +33,7 @@ class ShortUrlsParamsInputFilter extends InputFilter $this->add($this->createInput(self::SEARCH_TERM, false)); $this->add($this->createNumericInput(self::PAGE, false)); - $this->add($this->createNumericInput(self::ITEMS_PER_PAGE, false, -1)); + $this->add($this->createNumericInput(self::ITEMS_PER_PAGE, false, Paginator::ALL_ITEMS)); $this->add($this->createTagsInput(self::TAGS, false)); } diff --git a/module/Rest/test-api/Action/OrphanVisitsTest.php b/module/Rest/test-api/Action/OrphanVisitsTest.php index 21f4cae1..a37193da 100644 --- a/module/Rest/test-api/Action/OrphanVisitsTest.php +++ b/module/Rest/test-api/Action/OrphanVisitsTest.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace ShlinkioApiTest\Shlink\Rest\Action; use GuzzleHttp\RequestOptions; +use Shlinkio\Shlink\Common\Paginator\Paginator; use Shlinkio\Shlink\TestUtils\ApiTest\ApiTestCase; class OrphanVisitsTest extends ApiTestCase @@ -51,7 +52,7 @@ class OrphanVisitsTest extends ApiTestCase $payload = $this->getJsonResponsePayload($resp); $visits = $payload['visits']['data'] ?? []; - self::assertEquals($totalItems, $payload['visits']['pagination']['totalItems'] ?? -1); + self::assertEquals($totalItems, $payload['visits']['pagination']['totalItems'] ?? Paginator::ALL_ITEMS); self::assertCount($expectedAmount, $visits); self::assertEquals($expectedVisits, $visits); } diff --git a/module/Rest/test-api/Action/ShortUrlVisitsTest.php b/module/Rest/test-api/Action/ShortUrlVisitsTest.php index 327c7c05..a9e571da 100644 --- a/module/Rest/test-api/Action/ShortUrlVisitsTest.php +++ b/module/Rest/test-api/Action/ShortUrlVisitsTest.php @@ -6,6 +6,7 @@ namespace ShlinkioApiTest\Shlink\Rest\Action; use GuzzleHttp\Psr7\Query; use Laminas\Diactoros\Uri; +use Shlinkio\Shlink\Common\Paginator\Paginator; use Shlinkio\Shlink\TestUtils\ApiTest\ApiTestCase; use ShlinkioApiTest\Shlink\Rest\Utils\NotFoundUrlHelpersTrait; @@ -58,7 +59,10 @@ class ShortUrlVisitsTest extends ApiTestCase $resp = $this->callApiWithKey(self::METHOD_GET, (string) $url); $payload = $this->getJsonResponsePayload($resp); - self::assertEquals($expectedAmountOfVisits, $payload['visits']['pagination']['totalItems'] ?? -1); + self::assertEquals( + $expectedAmountOfVisits, + $payload['visits']['pagination']['totalItems'] ?? Paginator::ALL_ITEMS, + ); self::assertCount($expectedAmountOfVisits, $payload['visits']['data'] ?? []); } @@ -84,7 +88,10 @@ class ShortUrlVisitsTest extends ApiTestCase $resp = $this->callApiWithKey(self::METHOD_GET, (string) $url); $payload = $this->getJsonResponsePayload($resp); - self::assertEquals($expectedAmountOfVisits, $payload['visits']['pagination']['totalItems'] ?? -1); + self::assertEquals( + $expectedAmountOfVisits, + $payload['visits']['pagination']['totalItems'] ?? Paginator::ALL_ITEMS, + ); self::assertCount($expectedAmountOfVisits, $payload['visits']['data'] ?? []); } diff --git a/phpstan.neon b/phpstan.neon index bf3afc8e..0a2433e3 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -9,3 +9,6 @@ parameters: doctrine: repositoryClass: Happyr\DoctrineSpecification\Repository\EntitySpecificationRepository objectManagerLoader: 'config/entity-manager.php' + ignoreErrors: + - '#should return int<0, max> but returns int#' + - '#expects -1|int<1, max>, int given#' From 6c01bb87bfab9eefeb6f47f74e44d3e045ccc63c Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Wed, 8 Dec 2021 17:52:17 +0100 Subject: [PATCH 24/66] Replaced tabs by spaces in phpstan.neon config --- phpstan.neon | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/phpstan.neon b/phpstan.neon index 0a2433e3..aa5dab08 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,14 +1,14 @@ includes: - - vendor/phpstan/phpstan-doctrine/extension.neon - - vendor/phpstan/phpstan-symfony/extension.neon + - vendor/phpstan/phpstan-doctrine/extension.neon + - vendor/phpstan/phpstan-symfony/extension.neon parameters: - checkMissingIterableValueType: false - checkGenericClassInNonGenericObjectType: false - symfony: - console_application_loader: 'config/cli-app.php' - doctrine: - repositoryClass: Happyr\DoctrineSpecification\Repository\EntitySpecificationRepository - objectManagerLoader: 'config/entity-manager.php' - ignoreErrors: - - '#should return int<0, max> but returns int#' - - '#expects -1|int<1, max>, int given#' + checkMissingIterableValueType: false + checkGenericClassInNonGenericObjectType: false + symfony: + console_application_loader: 'config/cli-app.php' + doctrine: + repositoryClass: Happyr\DoctrineSpecification\Repository\EntitySpecificationRepository + objectManagerLoader: 'config/entity-manager.php' + ignoreErrors: + - '#should return int<0, max> but returns int#' + - '#expects -1|int<1, max>, int given#' From f8a48c16f0f5aeb88082d5b8d519fa8fbbf5823f Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Thu, 9 Dec 2021 09:45:15 +0100 Subject: [PATCH 25/66] Renamed GenerateShortUrlCommand to CreateShortUrlCommand --- module/CLI/config/cli.config.php | 2 +- module/CLI/config/dependencies.config.php | 4 ++-- ...enerateShortUrlCommand.php => CreateShortUrlCommand.php} | 5 +++-- ...hortUrlCommandTest.php => CreateShortUrlCommandTest.php} | 6 +++--- 4 files changed, 9 insertions(+), 8 deletions(-) rename module/CLI/src/Command/ShortUrl/{GenerateShortUrlCommand.php => CreateShortUrlCommand.php} (98%) rename module/CLI/test/Command/ShortUrl/{GenerateShortUrlCommandTest.php => CreateShortUrlCommandTest.php} (95%) diff --git a/module/CLI/config/cli.config.php b/module/CLI/config/cli.config.php index 46bb90ef..e06ad727 100644 --- a/module/CLI/config/cli.config.php +++ b/module/CLI/config/cli.config.php @@ -8,7 +8,7 @@ return [ 'cli' => [ 'commands' => [ - Command\ShortUrl\GenerateShortUrlCommand::NAME => Command\ShortUrl\GenerateShortUrlCommand::class, + Command\ShortUrl\CreateShortUrlCommand::NAME => Command\ShortUrl\CreateShortUrlCommand::class, Command\ShortUrl\ResolveUrlCommand::NAME => Command\ShortUrl\ResolveUrlCommand::class, Command\ShortUrl\ListShortUrlsCommand::NAME => Command\ShortUrl\ListShortUrlsCommand::class, Command\ShortUrl\GetVisitsCommand::NAME => Command\ShortUrl\GetVisitsCommand::class, diff --git a/module/CLI/config/dependencies.config.php b/module/CLI/config/dependencies.config.php index d89a8af2..cde351ee 100644 --- a/module/CLI/config/dependencies.config.php +++ b/module/CLI/config/dependencies.config.php @@ -39,7 +39,7 @@ return [ ApiKey\RoleResolver::class => ConfigAbstractFactory::class, - Command\ShortUrl\GenerateShortUrlCommand::class => ConfigAbstractFactory::class, + Command\ShortUrl\CreateShortUrlCommand::class => ConfigAbstractFactory::class, Command\ShortUrl\ResolveUrlCommand::class => ConfigAbstractFactory::class, Command\ShortUrl\ListShortUrlsCommand::class => ConfigAbstractFactory::class, Command\ShortUrl\GetVisitsCommand::class => ConfigAbstractFactory::class, @@ -75,7 +75,7 @@ return [ Util\ProcessRunner::class => [SymfonyCli\Helper\ProcessHelper::class], ApiKey\RoleResolver::class => [DomainService::class], - Command\ShortUrl\GenerateShortUrlCommand::class => [ + Command\ShortUrl\CreateShortUrlCommand::class => [ Service\UrlShortener::class, ShortUrlStringifier::class, 'config.url_shortener.default_short_codes_length', diff --git a/module/CLI/src/Command/ShortUrl/GenerateShortUrlCommand.php b/module/CLI/src/Command/ShortUrl/CreateShortUrlCommand.php similarity index 98% rename from module/CLI/src/Command/ShortUrl/GenerateShortUrlCommand.php rename to module/CLI/src/Command/ShortUrl/CreateShortUrlCommand.php index e43b4ec5..26673c6c 100644 --- a/module/CLI/src/Command/ShortUrl/GenerateShortUrlCommand.php +++ b/module/CLI/src/Command/ShortUrl/CreateShortUrlCommand.php @@ -26,9 +26,9 @@ use function method_exists; use function sprintf; use function str_contains; -class GenerateShortUrlCommand extends BaseCommand +class CreateShortUrlCommand extends BaseCommand { - public const NAME = 'short-url:generate'; + public const NAME = 'short-url:create'; public function __construct( private UrlShortenerInterface $urlShortener, @@ -42,6 +42,7 @@ class GenerateShortUrlCommand extends BaseCommand { $this ->setName(self::NAME) + ->setAliases(['short-url:generate']) // Deprecated ->setDescription('Generates a short URL for provided long URL and returns it') ->addArgument('longUrl', InputArgument::REQUIRED, 'The long URL to parse') ->addOption( diff --git a/module/CLI/test/Command/ShortUrl/GenerateShortUrlCommandTest.php b/module/CLI/test/Command/ShortUrl/CreateShortUrlCommandTest.php similarity index 95% rename from module/CLI/test/Command/ShortUrl/GenerateShortUrlCommandTest.php rename to module/CLI/test/Command/ShortUrl/CreateShortUrlCommandTest.php index 19767dc7..14d75f76 100644 --- a/module/CLI/test/Command/ShortUrl/GenerateShortUrlCommandTest.php +++ b/module/CLI/test/Command/ShortUrl/CreateShortUrlCommandTest.php @@ -8,7 +8,7 @@ use PHPUnit\Framework\Assert; use PHPUnit\Framework\TestCase; use Prophecy\Argument; use Prophecy\Prophecy\ObjectProphecy; -use Shlinkio\Shlink\CLI\Command\ShortUrl\GenerateShortUrlCommand; +use Shlinkio\Shlink\CLI\Command\ShortUrl\CreateShortUrlCommand; use Shlinkio\Shlink\CLI\Util\ExitCodes; use Shlinkio\Shlink\Core\Entity\ShortUrl; use Shlinkio\Shlink\Core\Exception\InvalidUrlException; @@ -19,7 +19,7 @@ use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifierInterface; use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait; use Symfony\Component\Console\Tester\CommandTester; -class GenerateShortUrlCommandTest extends TestCase +class CreateShortUrlCommandTest extends TestCase { use CliTestUtilsTrait; @@ -33,7 +33,7 @@ class GenerateShortUrlCommandTest extends TestCase $this->stringifier = $this->prophesize(ShortUrlStringifierInterface::class); $this->stringifier->stringify(Argument::type(ShortUrl::class))->willReturn(''); - $command = new GenerateShortUrlCommand($this->urlShortener->reveal(), $this->stringifier->reveal(), 5); + $command = new CreateShortUrlCommand($this->urlShortener->reveal(), $this->stringifier->reveal(), 5); $this->commandTester = $this->testerForCommand($command); } From cbd4b4849fb52473155cab0ca5f68be1e621323b Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Thu, 9 Dec 2021 10:24:58 +0100 Subject: [PATCH 26/66] Ensured default domain is stripped when creating short URLs from CLI --- module/CLI/config/dependencies.config.php | 1 + .../ShortUrl/CreateShortUrlCommand.php | 24 +++++++++++- .../ShortUrl/CreateShortUrlCommandTest.php | 37 ++++++++++++++++++- 3 files changed, 59 insertions(+), 3 deletions(-) diff --git a/module/CLI/config/dependencies.config.php b/module/CLI/config/dependencies.config.php index cde351ee..41d415dc 100644 --- a/module/CLI/config/dependencies.config.php +++ b/module/CLI/config/dependencies.config.php @@ -79,6 +79,7 @@ return [ Service\UrlShortener::class, ShortUrlStringifier::class, 'config.url_shortener.default_short_codes_length', + 'config.url_shortener.domain.hostname', ], Command\ShortUrl\ResolveUrlCommand::class => [Service\ShortUrl\ShortUrlResolver::class], Command\ShortUrl\ListShortUrlsCommand::class => [ diff --git a/module/CLI/src/Command/ShortUrl/CreateShortUrlCommand.php b/module/CLI/src/Command/ShortUrl/CreateShortUrlCommand.php index 26673c6c..62b50456 100644 --- a/module/CLI/src/Command/ShortUrl/CreateShortUrlCommand.php +++ b/module/CLI/src/Command/ShortUrl/CreateShortUrlCommand.php @@ -30,10 +30,13 @@ class CreateShortUrlCommand extends BaseCommand { public const NAME = 'short-url:create'; + private ?SymfonyStyle $io; + public function __construct( private UrlShortenerInterface $urlShortener, private ShortUrlStringifierInterface $stringifier, private int $defaultShortCodeLength, + private string $defaultDomain, ) { parent::__construct(); } @@ -123,21 +126,33 @@ class CreateShortUrlCommand extends BaseCommand protected function interact(InputInterface $input, OutputInterface $output): void { - $io = new SymfonyStyle($input, $output); + $this->verifyLongUrlArgument($input, $output); + $this->verifyDomainArgument($input); + } + + private function verifyLongUrlArgument(InputInterface $input, OutputInterface $output): void + { $longUrl = $input->getArgument('longUrl'); if (! empty($longUrl)) { return; } + $io = $this->getIO($input, $output); $longUrl = $io->ask('Which URL do you want to shorten?'); if (! empty($longUrl)) { $input->setArgument('longUrl', $longUrl); } } + private function verifyDomainArgument(InputInterface $input): void + { + $domain = $input->getOption('domain'); + $input->setOption('domain', $domain === $this->defaultDomain ? null : $domain); + } + protected function execute(InputInterface $input, OutputInterface $output): ?int { - $io = new SymfonyStyle($input, $output); + $io = $this->getIO($input, $output); $longUrl = $input->getArgument('longUrl'); if (empty($longUrl)) { $io->error('A URL was not provided!'); @@ -197,4 +212,9 @@ class CreateShortUrlCommand extends BaseCommand return null; } + + private function getIO(InputInterface $input, OutputInterface $output): SymfonyStyle + { + return $this->io ?? ($this->io = new SymfonyStyle($input, $output)); + } } diff --git a/module/CLI/test/Command/ShortUrl/CreateShortUrlCommandTest.php b/module/CLI/test/Command/ShortUrl/CreateShortUrlCommandTest.php index 14d75f76..08389d61 100644 --- a/module/CLI/test/Command/ShortUrl/CreateShortUrlCommandTest.php +++ b/module/CLI/test/Command/ShortUrl/CreateShortUrlCommandTest.php @@ -23,6 +23,8 @@ class CreateShortUrlCommandTest extends TestCase { use CliTestUtilsTrait; + private const DEFAULT_DOMAIN = 'default.com'; + private CommandTester $commandTester; private ObjectProphecy $urlShortener; private ObjectProphecy $stringifier; @@ -33,7 +35,12 @@ class CreateShortUrlCommandTest extends TestCase $this->stringifier = $this->prophesize(ShortUrlStringifierInterface::class); $this->stringifier->stringify(Argument::type(ShortUrl::class))->willReturn(''); - $command = new CreateShortUrlCommand($this->urlShortener->reveal(), $this->stringifier->reveal(), 5); + $command = new CreateShortUrlCommand( + $this->urlShortener->reveal(), + $this->stringifier->reveal(), + 5, + self::DEFAULT_DOMAIN, + ); $this->commandTester = $this->testerForCommand($command); } @@ -110,6 +117,34 @@ class CreateShortUrlCommandTest extends TestCase $stringify->shouldHaveBeenCalledOnce(); } + /** + * @test + * @dataProvider provideDomains + */ + public function properlyProcessesProvidedDomain(array $input, ?string $expectedDomain): void + { + $shorten = $this->urlShortener->shorten( + Argument::that(function (ShortUrlMeta $meta) use ($expectedDomain) { + Assert::assertEquals($expectedDomain, $meta->getDomain()); + return true; + }), + )->willReturn(ShortUrl::createEmpty()); + + $input['longUrl'] = 'http://domain.com/foo/bar'; + $this->commandTester->execute($input); + + self::assertEquals(ExitCodes::EXIT_SUCCESS, $this->commandTester->getStatusCode()); + $shorten->shouldHaveBeenCalledOnce(); + } + + public function provideDomains(): iterable + { + yield 'no domain' => [[], null]; + yield 'non-default domain foo' => [['--domain' => 'foo.com'], 'foo.com']; + yield 'non-default domain bar' => [['-d' => 'bar.com'], 'bar.com']; + yield 'default domain' => [['--domain' => self::DEFAULT_DOMAIN], null]; + } + /** * @test * @dataProvider provideFlags From 0b22fb933ccedde30f7d7d7fee8bae56cd029463 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Thu, 9 Dec 2021 10:30:33 +0100 Subject: [PATCH 27/66] Defined new env vars for not-found redirects, deprecating old ones --- config/autoload/redirects.global.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/config/autoload/redirects.global.php b/config/autoload/redirects.global.php index 339ca27d..d2c73884 100644 --- a/config/autoload/redirects.global.php +++ b/config/autoload/redirects.global.php @@ -10,9 +10,10 @@ use const Shlinkio\Shlink\DEFAULT_REDIRECT_STATUS_CODE; return [ 'not_found_redirects' => [ - 'invalid_short_url' => env('INVALID_SHORT_URL_REDIRECT_TO'), - 'regular_404' => env('REGULAR_404_REDIRECT_TO'), - 'base_url' => env('BASE_URL_REDIRECT_TO'), + // Deprecated env vars + 'invalid_short_url' => env('DEFAULT_INVALID_SHORT_URL_REDIRECT', env('INVALID_SHORT_URL_REDIRECT_TO')), + 'regular_404' => env('DEFAULT_REGULAR_404_REDIRECT', env('REGULAR_404_REDIRECT_TO')), + 'base_url' => env('DEFAULT_BASE_URL_REDIRECT', env('BASE_URL_REDIRECT_TO')), ], 'url_shortener' => [ From 348ac78f5af1e14f2470453d4217cb3c7cec69fc Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Thu, 9 Dec 2021 12:11:09 +0100 Subject: [PATCH 28/66] Enhanced ListDomainsAction so that it returns default redirects in the response --- docs/swagger/paths/v2_domains.json | 10 ++++++- .../src/Options/NotFoundRedirectOptions.php | 12 ++++++++- module/Rest/config/dependencies.config.php | 6 ++--- .../src/Action/Domain/ListDomainsAction.php | 4 ++- .../test-api/Action/DomainRedirectsTest.php | 26 ++++++------------- .../Rest/test-api/Action/ListDomainsTest.php | 5 ++++ .../Action/Domain/ListDomainsActionTest.php | 5 +++- 7 files changed, 43 insertions(+), 25 deletions(-) diff --git a/docs/swagger/paths/v2_domains.json b/docs/swagger/paths/v2_domains.json index ef63ee4e..40448016 100644 --- a/docs/swagger/paths/v2_domains.json +++ b/docs/swagger/paths/v2_domains.json @@ -46,6 +46,9 @@ } } } + }, + "defaultRedirects": { + "$ref": "../definitions/NotFoundRedirects.json" } } } @@ -84,7 +87,12 @@ "invalidShortUrlRedirect": "https://example.com/invalid-url" } } - ] + ], + "defaultRedirects": { + "baseUrlRedirect": "https://somewhere.com", + "regular404Redirect": null, + "invalidShortUrlRedirect": null + } } } } diff --git a/module/Core/src/Options/NotFoundRedirectOptions.php b/module/Core/src/Options/NotFoundRedirectOptions.php index 2f2d813b..27f410a8 100644 --- a/module/Core/src/Options/NotFoundRedirectOptions.php +++ b/module/Core/src/Options/NotFoundRedirectOptions.php @@ -4,10 +4,11 @@ declare(strict_types=1); namespace Shlinkio\Shlink\Core\Options; +use JsonSerializable; use Laminas\Stdlib\AbstractOptions; use Shlinkio\Shlink\Core\Config\NotFoundRedirectConfigInterface; -class NotFoundRedirectOptions extends AbstractOptions implements NotFoundRedirectConfigInterface +class NotFoundRedirectOptions extends AbstractOptions implements NotFoundRedirectConfigInterface, JsonSerializable { private ?string $invalidShortUrl = null; private ?string $regular404 = null; @@ -60,4 +61,13 @@ class NotFoundRedirectOptions extends AbstractOptions implements NotFoundRedirec $this->baseUrl = $baseUrl; return $this; } + + public function jsonSerialize(): array + { + return [ + 'baseUrlRedirect' => $this->baseUrl, + 'regular404Redirect' => $this->regular404, + 'invalidShortUrlRedirect' => $this->invalidShortUrl, + ]; + } } diff --git a/module/Rest/config/dependencies.config.php b/module/Rest/config/dependencies.config.php index 5e0267d6..98b385b0 100644 --- a/module/Rest/config/dependencies.config.php +++ b/module/Rest/config/dependencies.config.php @@ -9,7 +9,7 @@ use Laminas\ServiceManager\Factory\InvokableFactory; use Mezzio\Router\Middleware\ImplicitOptionsMiddleware; use Shlinkio\Shlink\Common\Mercure\LcobucciJwtProvider; use Shlinkio\Shlink\Core\Domain\DomainService; -use Shlinkio\Shlink\Core\Options\AppOptions; +use Shlinkio\Shlink\Core\Options; use Shlinkio\Shlink\Core\Service; use Shlinkio\Shlink\Core\ShortUrl\Transformer\ShortUrlDataTransformer; use Shlinkio\Shlink\Core\Tag\TagService; @@ -55,7 +55,7 @@ return [ ConfigAbstractFactory::class => [ ApiKeyService::class => ['em'], - Action\HealthAction::class => ['em', AppOptions::class], + Action\HealthAction::class => ['em', Options\AppOptions::class], Action\MercureInfoAction::class => [LcobucciJwtProvider::class, 'config.mercure'], Action\ShortUrl\CreateShortUrlAction::class => [Service\UrlShortener::class, ShortUrlDataTransformer::class], Action\ShortUrl\SingleStepCreateShortUrlAction::class => [ @@ -81,7 +81,7 @@ return [ Action\Tag\DeleteTagsAction::class => [TagService::class], Action\Tag\CreateTagsAction::class => [TagService::class], Action\Tag\UpdateTagAction::class => [TagService::class], - Action\Domain\ListDomainsAction::class => [DomainService::class], + Action\Domain\ListDomainsAction::class => [DomainService::class, Options\NotFoundRedirectOptions::class], Action\Domain\DomainRedirectsAction::class => [DomainService::class], Middleware\CrossDomainMiddleware::class => ['config.cors'], diff --git a/module/Rest/src/Action/Domain/ListDomainsAction.php b/module/Rest/src/Action/Domain/ListDomainsAction.php index c8f9a475..11b9e151 100644 --- a/module/Rest/src/Action/Domain/ListDomainsAction.php +++ b/module/Rest/src/Action/Domain/ListDomainsAction.php @@ -8,6 +8,7 @@ use Laminas\Diactoros\Response\JsonResponse; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Shlinkio\Shlink\Core\Domain\DomainServiceInterface; +use Shlinkio\Shlink\Core\Options\NotFoundRedirectOptions; use Shlinkio\Shlink\Rest\Action\AbstractRestAction; use Shlinkio\Shlink\Rest\Middleware\AuthenticationMiddleware; @@ -16,7 +17,7 @@ class ListDomainsAction extends AbstractRestAction protected const ROUTE_PATH = '/domains'; protected const ROUTE_ALLOWED_METHODS = [self::METHOD_GET]; - public function __construct(private DomainServiceInterface $domainService) + public function __construct(private DomainServiceInterface $domainService, private NotFoundRedirectOptions $options) { } @@ -28,6 +29,7 @@ class ListDomainsAction extends AbstractRestAction return new JsonResponse([ 'domains' => [ 'data' => $domainItems, + 'defaultRedirects' => $this->options, ], ]); } diff --git a/module/Rest/test-api/Action/DomainRedirectsTest.php b/module/Rest/test-api/Action/DomainRedirectsTest.php index 987c09d6..fdeec3b3 100644 --- a/module/Rest/test-api/Action/DomainRedirectsTest.php +++ b/module/Rest/test-api/Action/DomainRedirectsTest.php @@ -9,24 +9,6 @@ use Shlinkio\Shlink\TestUtils\ApiTest\ApiTestCase; class DomainRedirectsTest extends ApiTestCase { - /** @test */ - public function anErrorIsReturnedWhenTryingToEditDefaultDomain(): void - { - $resp = $this->callApiWithKey(self::METHOD_PATCH, '/domains/redirects', [ - RequestOptions::JSON => ['domain' => 'doma.in'], - ]); - $payload = $this->getJsonResponsePayload($resp); - - self::assertEquals(self::STATUS_FORBIDDEN, $resp->getStatusCode()); - self::assertEquals(self::STATUS_FORBIDDEN, $payload['status']); - self::assertEquals('INVALID_DOMAIN', $payload['type']); - self::assertEquals( - 'You cannot configure default domain\'s redirects this way. Use the configuration or env vars.', - $payload['detail'], - ); - self::assertEquals('Invalid domain', $payload['title']); - } - /** * @test * @dataProvider provideInvalidDomains @@ -78,6 +60,14 @@ class DomainRedirectsTest extends ApiTestCase 'regular404Redirect' => 'foo.com', 'invalidShortUrlRedirect' => null, ]]; + yield 'default domain' => [[ + 'domain' => 'doma.in', + 'regular404Redirect' => 'foo-for-default.com', + ], [ + 'baseUrlRedirect' => null, + 'regular404Redirect' => 'foo-for-default.com', + 'invalidShortUrlRedirect' => null, + ]]; yield 'existing domain with redirects' => [[ 'domain' => 'detached-with-redirects.com', 'baseUrlRedirect' => null, diff --git a/module/Rest/test-api/Action/ListDomainsTest.php b/module/Rest/test-api/Action/ListDomainsTest.php index 5f33c20b..54039c41 100644 --- a/module/Rest/test-api/Action/ListDomainsTest.php +++ b/module/Rest/test-api/Action/ListDomainsTest.php @@ -21,6 +21,11 @@ class ListDomainsTest extends ApiTestCase self::assertEquals([ 'domains' => [ 'data' => $expectedDomains, + 'defaultRedirects' => [ + 'baseUrlRedirect' => null, + 'regular404Redirect' => null, + 'invalidShortUrlRedirect' => null, + ], ], ], $respPayload); } diff --git a/module/Rest/test/Action/Domain/ListDomainsActionTest.php b/module/Rest/test/Action/Domain/ListDomainsActionTest.php index 45575cc6..9bbe9723 100644 --- a/module/Rest/test/Action/Domain/ListDomainsActionTest.php +++ b/module/Rest/test/Action/Domain/ListDomainsActionTest.php @@ -22,11 +22,13 @@ class ListDomainsActionTest extends TestCase private ListDomainsAction $action; private ObjectProphecy $domainService; + private NotFoundRedirectOptions $options; public function setUp(): void { $this->domainService = $this->prophesize(DomainServiceInterface::class); - $this->action = new ListDomainsAction($this->domainService->reveal()); + $this->options = new NotFoundRedirectOptions(); + $this->action = new ListDomainsAction($this->domainService->reveal(), $this->options); } /** @test */ @@ -46,6 +48,7 @@ class ListDomainsActionTest extends TestCase self::assertEquals([ 'domains' => [ 'data' => $domains, + 'defaultRedirects' => $this->options, ], ], $payload); $listDomains->shouldHaveBeenCalledOnce(); From ee43e68a57115b4e8395b45cc7496419dcf8961d Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Thu, 9 Dec 2021 12:32:02 +0100 Subject: [PATCH 29/66] Changed behavior of domains list so that it does not return configured redirects as redirects for default domain --- CHANGELOG.md | 8 ++++ .../Domain/DomainRedirectsCommandTest.php | 8 ++-- .../Command/Domain/ListDomainsCommandTest.php | 4 +- module/Core/config/dependencies.config.php | 6 +-- .../Config/EmptyNotFoundRedirectConfig.php | 38 +++++++++++++++++++ module/Core/src/Domain/DomainService.php | 13 +++---- module/Core/src/Domain/Model/DomainItem.php | 2 +- .../src/Options/NotFoundRedirectOptions.php | 12 +----- .../EmptyNotFoundRedirectConfigTest.php | 29 ++++++++++++++ module/Core/test/Domain/DomainServiceTest.php | 24 ++++++------ .../src/Action/Domain/ListDomainsAction.php | 3 +- .../Action/Domain/ListDomainsActionTest.php | 5 ++- 12 files changed, 106 insertions(+), 46 deletions(-) create mode 100644 module/Core/src/Config/EmptyNotFoundRedirectConfig.php create mode 100644 module/Core/test/Config/EmptyNotFoundRedirectConfigTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index dc6cdef3..9aeca402 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this ## [Unreleased] ### Added +* [#1163](https://github.com/shlinkio/shlink/issues/1163) Allowed setting not-found redirects for default domain in the same way it's done for any other domain. + + This implies a few non-breaking changes: + + * The domains list no longer has the values of `INVALID_SHORT_URL_REDIRECT_TO`, `REGULAR_404_REDIRECT_TO` and `BASE_URL_REDIRECT_TO` on the default domain redirects. + * The `GET /domains` endpoint includes a new `defaultRedirects` property in the response, with the default redirects set via config or env vars. + * The `INVALID_SHORT_URL_REDIRECT_TO`, `REGULAR_404_REDIRECT_TO` and `BASE_URL_REDIRECT_TO` env vars are now deprecated, and should be replaced by `DEFAULT_INVALID_SHORT_URL_REDIRECT`, `DEFAULT_REGULAR_404_REDIRECT` and `DEFAULT_BASE_URL_REDIRECT` respectively. Deprecated ones will continue to work until v3.0.0, where they will be removed. + * [#1204](https://github.com/shlinkio/shlink/issues/1204) Added support for `openswoole` and migrated official docker image to `openswoole`. * [#1242](https://github.com/shlinkio/shlink/issues/1242) Added support to import urls and visits from YOURLS. diff --git a/module/CLI/test/Command/Domain/DomainRedirectsCommandTest.php b/module/CLI/test/Command/Domain/DomainRedirectsCommandTest.php index 9801930e..6b6e1036 100644 --- a/module/CLI/test/Command/Domain/DomainRedirectsCommandTest.php +++ b/module/CLI/test/Command/Domain/DomainRedirectsCommandTest.php @@ -126,8 +126,8 @@ class DomainRedirectsCommandTest extends TestCase $listDomains = $this->domainService->listDomains()->willReturn([ DomainItem::forDefaultDomain('default-domain.com', new NotFoundRedirectOptions()), - DomainItem::forExistingDomain(Domain::withAuthority('existing-one.com')), - DomainItem::forExistingDomain(Domain::withAuthority($domainAuthority)), + DomainItem::forNonDefaultDomain(Domain::withAuthority('existing-one.com')), + DomainItem::forNonDefaultDomain(Domain::withAuthority($domainAuthority)), ]); $findDomain = $this->domainService->findByAuthority($domainAuthority)->willReturn($domain); $configureRedirects = $this->domainService->configureNotFoundRedirects( @@ -156,8 +156,8 @@ class DomainRedirectsCommandTest extends TestCase $listDomains = $this->domainService->listDomains()->willReturn([ DomainItem::forDefaultDomain('default-domain.com', new NotFoundRedirectOptions()), - DomainItem::forExistingDomain(Domain::withAuthority('existing-one.com')), - DomainItem::forExistingDomain(Domain::withAuthority('existing-two.com')), + DomainItem::forNonDefaultDomain(Domain::withAuthority('existing-one.com')), + DomainItem::forNonDefaultDomain(Domain::withAuthority('existing-two.com')), ]); $findDomain = $this->domainService->findByAuthority($domainAuthority)->willReturn($domain); $configureRedirects = $this->domainService->configureNotFoundRedirects( diff --git a/module/CLI/test/Command/Domain/ListDomainsCommandTest.php b/module/CLI/test/Command/Domain/ListDomainsCommandTest.php index 13e6d062..6d56ea69 100644 --- a/module/CLI/test/Command/Domain/ListDomainsCommandTest.php +++ b/module/CLI/test/Command/Domain/ListDomainsCommandTest.php @@ -47,8 +47,8 @@ class ListDomainsCommandTest extends TestCase 'base_url' => 'https://foo.com/default/base', 'invalid_short_url' => 'https://foo.com/default/invalid', ])), - DomainItem::forExistingDomain(Domain::withAuthority('bar.com')), - DomainItem::forExistingDomain($bazDomain), + DomainItem::forNonDefaultDomain(Domain::withAuthority('bar.com')), + DomainItem::forNonDefaultDomain($bazDomain), ]); $this->commandTester->execute($input); diff --git a/module/Core/config/dependencies.config.php b/module/Core/config/dependencies.config.php index 16b84819..fdfecef9 100644 --- a/module/Core/config/dependencies.config.php +++ b/module/Core/config/dependencies.config.php @@ -119,11 +119,7 @@ return [ ], Service\ShortUrl\ShortUrlResolver::class => ['em'], Service\ShortUrl\ShortCodeHelper::class => ['em'], - Domain\DomainService::class => [ - 'em', - 'config.url_shortener.domain.hostname', - Options\NotFoundRedirectOptions::class, - ], + Domain\DomainService::class => ['em', 'config.url_shortener.domain.hostname'], Util\UrlValidator::class => ['httpClient', Options\UrlShortenerOptions::class], Util\DoctrineBatchHelper::class => ['em'], diff --git a/module/Core/src/Config/EmptyNotFoundRedirectConfig.php b/module/Core/src/Config/EmptyNotFoundRedirectConfig.php new file mode 100644 index 00000000..6ccb3848 --- /dev/null +++ b/module/Core/src/Config/EmptyNotFoundRedirectConfig.php @@ -0,0 +1,38 @@ +defaultDomainAndRest($apiKey); - $mappedDomains = map($domains, fn (Domain $domain) => DomainItem::forExistingDomain($domain)); + $mappedDomains = map($domains, fn (Domain $domain) => DomainItem::forNonDefaultDomain($domain)); if ($apiKey?->hasRole(Role::DOMAIN_SPECIFIC)) { return $mappedDomains; } return [ - DomainItem::forDefaultDomain($this->defaultDomain, $default ?? $this->redirectOptions), + DomainItem::forDefaultDomain($this->defaultDomain, $default ?? new EmptyNotFoundRedirectConfig()), ...$mappedDomains, ]; } diff --git a/module/Core/src/Domain/Model/DomainItem.php b/module/Core/src/Domain/Model/DomainItem.php index 909cca7d..5547fe8d 100644 --- a/module/Core/src/Domain/Model/DomainItem.php +++ b/module/Core/src/Domain/Model/DomainItem.php @@ -18,7 +18,7 @@ final class DomainItem implements JsonSerializable ) { } - public static function forExistingDomain(Domain $domain): self + public static function forNonDefaultDomain(Domain $domain): self { return new self($domain->getAuthority(), $domain, false); } diff --git a/module/Core/src/Options/NotFoundRedirectOptions.php b/module/Core/src/Options/NotFoundRedirectOptions.php index 27f410a8..2f2d813b 100644 --- a/module/Core/src/Options/NotFoundRedirectOptions.php +++ b/module/Core/src/Options/NotFoundRedirectOptions.php @@ -4,11 +4,10 @@ declare(strict_types=1); namespace Shlinkio\Shlink\Core\Options; -use JsonSerializable; use Laminas\Stdlib\AbstractOptions; use Shlinkio\Shlink\Core\Config\NotFoundRedirectConfigInterface; -class NotFoundRedirectOptions extends AbstractOptions implements NotFoundRedirectConfigInterface, JsonSerializable +class NotFoundRedirectOptions extends AbstractOptions implements NotFoundRedirectConfigInterface { private ?string $invalidShortUrl = null; private ?string $regular404 = null; @@ -61,13 +60,4 @@ class NotFoundRedirectOptions extends AbstractOptions implements NotFoundRedirec $this->baseUrl = $baseUrl; return $this; } - - public function jsonSerialize(): array - { - return [ - 'baseUrlRedirect' => $this->baseUrl, - 'regular404Redirect' => $this->regular404, - 'invalidShortUrlRedirect' => $this->invalidShortUrl, - ]; - } } diff --git a/module/Core/test/Config/EmptyNotFoundRedirectConfigTest.php b/module/Core/test/Config/EmptyNotFoundRedirectConfigTest.php new file mode 100644 index 00000000..d1c47e10 --- /dev/null +++ b/module/Core/test/Config/EmptyNotFoundRedirectConfigTest.php @@ -0,0 +1,29 @@ +redirectsConfig = new EmptyNotFoundRedirectConfig(); + } + + /** @test */ + public function allMethodsReturnHardcodedValues(): void + { + self::assertNull($this->redirectsConfig->invalidShortUrlRedirect()); + self::assertFalse($this->redirectsConfig->hasInvalidShortUrlRedirect()); + self::assertNull($this->redirectsConfig->regular404Redirect()); + self::assertFalse($this->redirectsConfig->hasRegular404Redirect()); + self::assertNull($this->redirectsConfig->baseUrlRedirect()); + self::assertFalse($this->redirectsConfig->hasBaseUrlRedirect()); + } +} diff --git a/module/Core/test/Domain/DomainServiceTest.php b/module/Core/test/Domain/DomainServiceTest.php index 337438b5..71922fe3 100644 --- a/module/Core/test/Domain/DomainServiceTest.php +++ b/module/Core/test/Domain/DomainServiceTest.php @@ -9,13 +9,13 @@ use PHPUnit\Framework\TestCase; use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; use Prophecy\Prophecy\ObjectProphecy; +use Shlinkio\Shlink\Core\Config\EmptyNotFoundRedirectConfig; use Shlinkio\Shlink\Core\Config\NotFoundRedirects; use Shlinkio\Shlink\Core\Domain\DomainService; use Shlinkio\Shlink\Core\Domain\Model\DomainItem; use Shlinkio\Shlink\Core\Domain\Repository\DomainRepositoryInterface; use Shlinkio\Shlink\Core\Entity\Domain; use Shlinkio\Shlink\Core\Exception\DomainNotFoundException; -use Shlinkio\Shlink\Core\Options\NotFoundRedirectOptions; use Shlinkio\Shlink\Rest\ApiKey\Model\ApiKeyMeta; use Shlinkio\Shlink\Rest\ApiKey\Model\RoleDefinition; use Shlinkio\Shlink\Rest\Entity\ApiKey; @@ -30,7 +30,7 @@ class DomainServiceTest extends TestCase public function setUp(): void { $this->em = $this->prophesize(EntityManagerInterface::class); - $this->domainService = new DomainService($this->em->reveal(), 'default.com', new NotFoundRedirectOptions()); + $this->domainService = new DomainService($this->em->reveal(), 'default.com'); } /** @@ -52,7 +52,7 @@ class DomainServiceTest extends TestCase public function provideExcludedDomains(): iterable { - $default = DomainItem::forDefaultDomain('default.com', new NotFoundRedirectOptions()); + $default = DomainItem::forDefaultDomain('default.com', new EmptyNotFoundRedirectConfig()); $adminApiKey = ApiKey::create(); $domainSpecificApiKey = ApiKey::fromMeta( ApiKeyMeta::withRoles(RoleDefinition::forDomain(Domain::withAuthority('')->setId('123'))), @@ -61,15 +61,15 @@ class DomainServiceTest extends TestCase yield 'empty list without API key' => [[], [$default], null]; yield 'one item without API key' => [ [Domain::withAuthority('bar.com')], - [$default, DomainItem::forExistingDomain(Domain::withAuthority('bar.com'))], + [$default, DomainItem::forNonDefaultDomain(Domain::withAuthority('bar.com'))], null, ]; yield 'multiple items without API key' => [ [Domain::withAuthority('foo.com'), Domain::withAuthority('bar.com')], [ $default, - DomainItem::forExistingDomain(Domain::withAuthority('foo.com')), - DomainItem::forExistingDomain(Domain::withAuthority('bar.com')), + DomainItem::forNonDefaultDomain(Domain::withAuthority('foo.com')), + DomainItem::forNonDefaultDomain(Domain::withAuthority('bar.com')), ], null, ]; @@ -77,15 +77,15 @@ class DomainServiceTest extends TestCase yield 'empty list with admin API key' => [[], [$default], $adminApiKey]; yield 'one item with admin API key' => [ [Domain::withAuthority('bar.com')], - [$default, DomainItem::forExistingDomain(Domain::withAuthority('bar.com'))], + [$default, DomainItem::forNonDefaultDomain(Domain::withAuthority('bar.com'))], $adminApiKey, ]; yield 'multiple items with admin API key' => [ [Domain::withAuthority('foo.com'), Domain::withAuthority('bar.com')], [ $default, - DomainItem::forExistingDomain(Domain::withAuthority('foo.com')), - DomainItem::forExistingDomain(Domain::withAuthority('bar.com')), + DomainItem::forNonDefaultDomain(Domain::withAuthority('foo.com')), + DomainItem::forNonDefaultDomain(Domain::withAuthority('bar.com')), ], $adminApiKey, ]; @@ -93,14 +93,14 @@ class DomainServiceTest extends TestCase yield 'empty list with domain-specific API key' => [[], [], $domainSpecificApiKey]; yield 'one item with domain-specific API key' => [ [Domain::withAuthority('bar.com')], - [DomainItem::forExistingDomain(Domain::withAuthority('bar.com'))], + [DomainItem::forNonDefaultDomain(Domain::withAuthority('bar.com'))], $domainSpecificApiKey, ]; yield 'multiple items with domain-specific API key' => [ [Domain::withAuthority('foo.com'), Domain::withAuthority('bar.com')], [ - DomainItem::forExistingDomain(Domain::withAuthority('foo.com')), - DomainItem::forExistingDomain(Domain::withAuthority('bar.com')), + DomainItem::forNonDefaultDomain(Domain::withAuthority('foo.com')), + DomainItem::forNonDefaultDomain(Domain::withAuthority('bar.com')), ], $domainSpecificApiKey, ]; diff --git a/module/Rest/src/Action/Domain/ListDomainsAction.php b/module/Rest/src/Action/Domain/ListDomainsAction.php index 11b9e151..e50ada16 100644 --- a/module/Rest/src/Action/Domain/ListDomainsAction.php +++ b/module/Rest/src/Action/Domain/ListDomainsAction.php @@ -7,6 +7,7 @@ namespace Shlinkio\Shlink\Rest\Action\Domain; use Laminas\Diactoros\Response\JsonResponse; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; +use Shlinkio\Shlink\Core\Config\NotFoundRedirects; use Shlinkio\Shlink\Core\Domain\DomainServiceInterface; use Shlinkio\Shlink\Core\Options\NotFoundRedirectOptions; use Shlinkio\Shlink\Rest\Action\AbstractRestAction; @@ -29,7 +30,7 @@ class ListDomainsAction extends AbstractRestAction return new JsonResponse([ 'domains' => [ 'data' => $domainItems, - 'defaultRedirects' => $this->options, + 'defaultRedirects' => NotFoundRedirects::fromConfig($this->options), ], ]); } diff --git a/module/Rest/test/Action/Domain/ListDomainsActionTest.php b/module/Rest/test/Action/Domain/ListDomainsActionTest.php index 9bbe9723..bc852b34 100644 --- a/module/Rest/test/Action/Domain/ListDomainsActionTest.php +++ b/module/Rest/test/Action/Domain/ListDomainsActionTest.php @@ -9,6 +9,7 @@ use Laminas\Diactoros\ServerRequestFactory; use PHPUnit\Framework\TestCase; use Prophecy\PhpUnit\ProphecyTrait; use Prophecy\Prophecy\ObjectProphecy; +use Shlinkio\Shlink\Core\Config\NotFoundRedirects; use Shlinkio\Shlink\Core\Domain\DomainServiceInterface; use Shlinkio\Shlink\Core\Domain\Model\DomainItem; use Shlinkio\Shlink\Core\Entity\Domain; @@ -37,7 +38,7 @@ class ListDomainsActionTest extends TestCase $apiKey = ApiKey::create(); $domains = [ DomainItem::forDefaultDomain('bar.com', new NotFoundRedirectOptions()), - DomainItem::forExistingDomain(Domain::withAuthority('baz.com')), + DomainItem::forNonDefaultDomain(Domain::withAuthority('baz.com')), ]; $listDomains = $this->domainService->listDomains($apiKey)->willReturn($domains); @@ -48,7 +49,7 @@ class ListDomainsActionTest extends TestCase self::assertEquals([ 'domains' => [ 'data' => $domains, - 'defaultRedirects' => $this->options, + 'defaultRedirects' => NotFoundRedirects::fromConfig($this->options), ], ], $payload); $listDomains->shouldHaveBeenCalledOnce(); From 9752abff1905c3e527a6162e85dbff8fdbfaba3e Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Thu, 9 Dec 2021 12:43:49 +0100 Subject: [PATCH 30/66] Refactored method in DomainRepo, as one fo their arguments was no longer used --- docs/swagger/paths/v2_domains.json | 4 +-- module/Core/src/Domain/DomainService.php | 5 ++- .../Domain/Repository/DomainRepository.php | 13 +++---- .../Repository/DomainRepositoryInterface.php | 2 +- .../Core/src/Domain/Spec/IsNotAuthority.php | 22 ------------ .../Repository/DomainRepositoryTest.php | 35 ++++--------------- module/Core/test/Domain/DomainServiceTest.php | 2 +- 7 files changed, 16 insertions(+), 67 deletions(-) delete mode 100644 module/Core/src/Domain/Spec/IsNotAuthority.php diff --git a/docs/swagger/paths/v2_domains.json b/docs/swagger/paths/v2_domains.json index 40448016..d92677c1 100644 --- a/docs/swagger/paths/v2_domains.json +++ b/docs/swagger/paths/v2_domains.json @@ -4,8 +4,8 @@ "tags": [ "Domains" ], - "summary": "List existing domains", - "description": "Returns the list of all domains ever used, with a flag that tells if they are the default domain", + "summary": "List configured domains", + "description": "Returns the list of all domains that have been either used for some short URL, or have explicitly configured redirects.
It also includes the domain redirects, plus the default redirects that will be used for any non-explicitly-configured one.", "security": [ { "ApiKey": [] diff --git a/module/Core/src/Domain/DomainService.php b/module/Core/src/Domain/DomainService.php index 743312b6..d5e5d88c 100644 --- a/module/Core/src/Domain/DomainService.php +++ b/module/Core/src/Domain/DomainService.php @@ -50,7 +50,7 @@ class DomainService implements DomainServiceInterface /** @var DomainRepositoryInterface $repo */ $repo = $this->em->getRepository(Domain::class); $groups = group( - $repo->findDomainsWithout(null, $apiKey), // FIXME Always called with null as first arg + $repo->findDomains($apiKey), fn (Domain $domain) => $domain->getAuthority() === $this->defaultDomain ? 'default' : 'domains', ); @@ -73,8 +73,7 @@ class DomainService implements DomainServiceInterface public function findByAuthority(string $authority, ?ApiKey $apiKey = null): ?Domain { - $repo = $this->em->getRepository(Domain::class); - return $repo->findOneByAuthority($authority, $apiKey); + return $this->em->getRepository(Domain::class)->findOneByAuthority($authority, $apiKey); } /** diff --git a/module/Core/src/Domain/Repository/DomainRepository.php b/module/Core/src/Domain/Repository/DomainRepository.php index 1741cea7..4de3ea36 100644 --- a/module/Core/src/Domain/Repository/DomainRepository.php +++ b/module/Core/src/Domain/Repository/DomainRepository.php @@ -8,7 +8,6 @@ use Doctrine\ORM\Query\Expr\Join; use Happyr\DoctrineSpecification\Repository\EntitySpecificationRepository; use Happyr\DoctrineSpecification\Spec; use Shlinkio\Shlink\Core\Domain\Spec\IsDomain; -use Shlinkio\Shlink\Core\Domain\Spec\IsNotAuthority; use Shlinkio\Shlink\Core\Entity\Domain; use Shlinkio\Shlink\Core\Entity\ShortUrl; use Shlinkio\Shlink\Core\ShortUrl\Spec\BelongsToApiKey; @@ -20,7 +19,7 @@ class DomainRepository extends EntitySpecificationRepository implements DomainRe /** * @return Domain[] */ - public function findDomainsWithout(?string $excludedAuthority, ?ApiKey $apiKey = null): array + public function findDomains(?ApiKey $apiKey = null): array { $qb = $this->createQueryBuilder('d'); $qb->leftJoin(ShortUrl::class, 's', Join::WITH, 's.domain = d') @@ -31,7 +30,7 @@ class DomainRepository extends EntitySpecificationRepository implements DomainRe ->orHaving($qb->expr()->isNotNull('d.regular404Redirect')) ->orHaving($qb->expr()->isNotNull('d.invalidShortUrlRedirect')); - $specs = $this->determineExtraSpecs($excludedAuthority, $apiKey); + $specs = $this->determineExtraSpecs($apiKey); foreach ($specs as [$alias, $spec]) { $this->applySpecification($qb, $spec, $alias); } @@ -47,7 +46,7 @@ class DomainRepository extends EntitySpecificationRepository implements DomainRe ->setParameter('authority', $authority) ->setMaxResults(1); - $specs = $this->determineExtraSpecs(null, $apiKey); + $specs = $this->determineExtraSpecs($apiKey); foreach ($specs as [$alias, $spec]) { $this->applySpecification($qb, $spec, $alias); } @@ -55,12 +54,8 @@ class DomainRepository extends EntitySpecificationRepository implements DomainRe return $qb->getQuery()->getOneOrNullResult(); } - private function determineExtraSpecs(?string $excludedAuthority, ?ApiKey $apiKey): iterable + private function determineExtraSpecs(?ApiKey $apiKey): iterable { - if ($excludedAuthority !== null) { - yield ['d', new IsNotAuthority($excludedAuthority)]; - } - // FIXME The $apiKey->spec() method cannot be used here, as it returns a single spec which assumes the // ShortUrl is the root entity. Here, the Domain is the root entity. // Think on a way to centralize the conditional behavior and make $apiKey->spec() more flexible. diff --git a/module/Core/src/Domain/Repository/DomainRepositoryInterface.php b/module/Core/src/Domain/Repository/DomainRepositoryInterface.php index 123e349d..69e74e5b 100644 --- a/module/Core/src/Domain/Repository/DomainRepositoryInterface.php +++ b/module/Core/src/Domain/Repository/DomainRepositoryInterface.php @@ -14,7 +14,7 @@ interface DomainRepositoryInterface extends ObjectRepository, EntitySpecificatio /** * @return Domain[] */ - public function findDomainsWithout(?string $excludedAuthority, ?ApiKey $apiKey = null): array; + public function findDomains(?ApiKey $apiKey = null): array; public function findOneByAuthority(string $authority, ?ApiKey $apiKey = null): ?Domain; } diff --git a/module/Core/src/Domain/Spec/IsNotAuthority.php b/module/Core/src/Domain/Spec/IsNotAuthority.php deleted file mode 100644 index 0f0f0653..00000000 --- a/module/Core/src/Domain/Spec/IsNotAuthority.php +++ /dev/null @@ -1,22 +0,0 @@ -authority)); - } -} diff --git a/module/Core/test-db/Domain/Repository/DomainRepositoryTest.php b/module/Core/test-db/Domain/Repository/DomainRepositoryTest.php index 1eaf6ea9..382e58dd 100644 --- a/module/Core/test-db/Domain/Repository/DomainRepositoryTest.php +++ b/module/Core/test-db/Domain/Repository/DomainRepositoryTest.php @@ -50,27 +50,7 @@ class DomainRepositoryTest extends DatabaseTestCase $this->getEntityManager()->flush(); - self::assertEquals( - [$barDomain, $bazDomain, $detachedWithRedirects, $fooDomain], - $this->repo->findDomainsWithout(null), - ); - self::assertEquals( - [$barDomain, $bazDomain, $detachedWithRedirects], - $this->repo->findDomainsWithout('foo.com'), - ); - self::assertEquals( - [$bazDomain, $detachedWithRedirects, $fooDomain], - $this->repo->findDomainsWithout('bar.com'), - ); - self::assertEquals( - [$barDomain, $detachedWithRedirects, $fooDomain], - $this->repo->findDomainsWithout('baz.com'), - ); - self::assertEquals( - [$barDomain, $bazDomain, $fooDomain], - $this->repo->findDomainsWithout('detached-with-redirects.com'), - ); - + self::assertEquals([$barDomain, $bazDomain, $detachedWithRedirects, $fooDomain], $this->repo->findDomains()); self::assertEquals($barDomain, $this->repo->findOneByAuthority('bar.com')); self::assertEquals($detachedWithRedirects, $this->repo->findOneByAuthority('detached-with-redirects.com')); self::assertNull($this->repo->findOneByAuthority('does-not-exist.com')); @@ -121,14 +101,11 @@ class DomainRepositoryTest extends DatabaseTestCase $this->getEntityManager()->flush(); - self::assertEquals([$fooDomain], $this->repo->findDomainsWithout(null, $fooDomainApiKey)); - self::assertEquals([$barDomain], $this->repo->findDomainsWithout(null, $barDomainApiKey)); - self::assertEquals( - [$detachedWithRedirects], - $this->repo->findDomainsWithout(null, $detachedWithRedirectsApiKey), - ); - self::assertEquals([$bazDomain, $fooDomain], $this->repo->findDomainsWithout(null, $authorApiKey)); - self::assertEquals([], $this->repo->findDomainsWithout(null, $authorAndDomainApiKey)); + self::assertEquals([$fooDomain], $this->repo->findDomains($fooDomainApiKey)); + self::assertEquals([$barDomain], $this->repo->findDomains($barDomainApiKey)); + self::assertEquals([$detachedWithRedirects], $this->repo->findDomains($detachedWithRedirectsApiKey)); + self::assertEquals([$bazDomain, $fooDomain], $this->repo->findDomains($authorApiKey)); + self::assertEquals([], $this->repo->findDomains($authorAndDomainApiKey)); self::assertEquals($fooDomain, $this->repo->findOneByAuthority('foo.com', $authorApiKey)); self::assertNull($this->repo->findOneByAuthority('bar.com', $authorApiKey)); diff --git a/module/Core/test/Domain/DomainServiceTest.php b/module/Core/test/Domain/DomainServiceTest.php index 71922fe3..ea3cfe02 100644 --- a/module/Core/test/Domain/DomainServiceTest.php +++ b/module/Core/test/Domain/DomainServiceTest.php @@ -41,7 +41,7 @@ class DomainServiceTest extends TestCase { $repo = $this->prophesize(DomainRepositoryInterface::class); $getRepo = $this->em->getRepository(Domain::class)->willReturn($repo->reveal()); - $findDomains = $repo->findDomainsWithout(null, $apiKey)->willReturn($domains); + $findDomains = $repo->findDomains($apiKey)->willReturn($domains); $result = $this->domainService->listDomains($apiKey); From 808ae6a442c22d453ca248df0e6f15d748753504 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Thu, 9 Dec 2021 15:27:18 +0100 Subject: [PATCH 31/66] Fixed existing examples for API --- docs/swagger/paths/health.json | 36 ++-- docs/swagger/paths/v1_short-urls.json | 173 +++++++++--------- docs/swagger/paths/v1_short-urls_shorten.json | 71 +++---- .../paths/v1_short-urls_{shortCode}.json | 116 +++++++----- .../paths/v1_short-urls_{shortCode}_tags.json | 8 - .../v1_short-urls_{shortCode}_visits.json | 80 ++++---- docs/swagger/paths/v1_tags.json | 60 +++--- docs/swagger/paths/v2_domains.json | 72 ++++---- docs/swagger/paths/v2_domains_redirects.json | 12 +- docs/swagger/paths/v2_mercure-info.json | 26 ++- docs/swagger/paths/v2_tags_{tag}_visits.json | 80 ++++---- docs/swagger/paths/v2_visits.json | 14 +- docs/swagger/paths/v2_visits_orphan.json | 92 +++++----- 13 files changed, 417 insertions(+), 423 deletions(-) diff --git a/docs/swagger/paths/health.json b/docs/swagger/paths/health.json index 60d96ccc..c9cb300f 100644 --- a/docs/swagger/paths/health.json +++ b/docs/swagger/paths/health.json @@ -13,16 +13,14 @@ "application/json": { "schema": { "$ref": "../definitions/Health.json" - } - } - }, - "examples": { - "application/json": { - "status": "pass", - "version": "1.16.0", - "links": { - "about": "https://shlink.io", - "project": "https://github.com/shlinkio/shlink" + }, + "example": { + "status": "pass", + "version": "2.10.0", + "links": { + "about": "https://shlink.io", + "project": "https://github.com/shlinkio/shlink" + } } } } @@ -33,16 +31,14 @@ "application/json": { "schema": { "$ref": "../definitions/Health.json" - } - } - }, - "examples": { - "application/json": { - "status": "fail", - "version": "1.16.0", - "links": { - "about": "https://shlink.io", - "project": "https://github.com/shlinkio/shlink" + }, + "example": { + "status": "fail", + "version": "2.10.0", + "links": { + "about": "https://shlink.io", + "project": "https://github.com/shlinkio/shlink" + } } } } diff --git a/docs/swagger/paths/v1_short-urls.json b/docs/swagger/paths/v1_short-urls.json index a4643058..ecef0285 100644 --- a/docs/swagger/paths/v1_short-urls.json +++ b/docs/swagger/paths/v1_short-urls.json @@ -117,73 +117,71 @@ } } } - } - } - }, - "examples": { - "application/json": { - "shortUrls": { - "data": [ - { - "shortCode": "12C18", - "shortUrl": "https://doma.in/12C18", - "longUrl": "https://store.steampowered.com", - "dateCreated": "2016-08-21T20:34:16+02:00", - "visitsCount": 328, - "tags": [ - "games", - "tech" - ], - "meta": { - "validSince": "2017-01-21T00:00:00+02:00", - "validUntil": null, - "maxVisits": 100 + }, + "example": { + "shortUrls": { + "data": [ + { + "shortCode": "12C18", + "shortUrl": "https://doma.in/12C18", + "longUrl": "https://store.steampowered.com", + "dateCreated": "2016-08-21T20:34:16+02:00", + "visitsCount": 328, + "tags": [ + "games", + "tech" + ], + "meta": { + "validSince": "2017-01-21T00:00:00+02:00", + "validUntil": null, + "maxVisits": 100 + }, + "domain": null, + "title": "Welcome to Steam", + "crawlable": false }, - "domain": null, - "title": "Welcome to Steam", - "crawlable": false - }, - { - "shortCode": "12Kb3", - "shortUrl": "https://doma.in/12Kb3", - "longUrl": "https://shlink.io", - "dateCreated": "2016-05-01T20:34:16+02:00", - "visitsCount": 1029, - "tags": [ - "shlink" - ], - "meta": { - "validSince": null, - "validUntil": null, - "maxVisits": null + { + "shortCode": "12Kb3", + "shortUrl": "https://doma.in/12Kb3", + "longUrl": "https://shlink.io", + "dateCreated": "2016-05-01T20:34:16+02:00", + "visitsCount": 1029, + "tags": [ + "shlink" + ], + "meta": { + "validSince": null, + "validUntil": null, + "maxVisits": null + }, + "domain": null, + "title": null, + "crawlable": false }, - "domain": null, - "title": null, - "crawlable": false - }, - { - "shortCode": "123bA", - "shortUrl": "https://example.com/123bA", - "longUrl": "https://www.google.com", - "dateCreated": "2015-10-01T20:34:16+02:00", - "visitsCount": 25, - "tags": [], - "meta": { - "validSince": "2017-01-21T00:00:00+02:00", - "validUntil": null, - "maxVisits": null - }, - "domain": "example.com", - "title": null, - "crawlable": false + { + "shortCode": "123bA", + "shortUrl": "https://example.com/123bA", + "longUrl": "https://www.google.com", + "dateCreated": "2015-10-01T20:34:16+02:00", + "visitsCount": 25, + "tags": [], + "meta": { + "validSince": "2017-01-21T00:00:00+02:00", + "validUntil": null, + "maxVisits": null + }, + "domain": "example.com", + "title": null, + "crawlable": false + } + ], + "pagination": { + "currentPage": 5, + "pagesCount": 12, + "itemsPerPage": 10, + "itemsInCurrentPage": 10, + "totalItems": 115 } - ], - "pagination": { - "currentPage": 5, - "pagesCount": 12, - "itemsPerPage": 10, - "itemsInCurrentPage": 10, - "totalItems": 115 } } } @@ -267,28 +265,26 @@ "application/json": { "schema": { "$ref": "../definitions/ShortUrl.json" - } - } - }, - "examples": { - "application/json": { - "shortCode": "12C18", - "shortUrl": "https://doma.in/12C18", - "longUrl": "https://store.steampowered.com", - "dateCreated": "2016-08-21T20:34:16+02:00", - "visitsCount": 0, - "tags": [ - "games", - "tech" - ], - "meta": { - "validSince": "2017-01-21T00:00:00+02:00", - "validUntil": null, - "maxVisits": 500 }, - "domain": null, - "title": null, - "crawlable": false + "example": { + "shortCode": "12C18", + "shortUrl": "https://doma.in/12C18", + "longUrl": "https://store.steampowered.com", + "dateCreated": "2016-08-21T20:34:16+02:00", + "visitsCount": 0, + "tags": [ + "games", + "tech" + ], + "meta": { + "validSince": "2017-01-21T00:00:00+02:00", + "validUntil": null, + "maxVisits": 500 + }, + "domain": null, + "title": null, + "crawlable": false + } } } }, @@ -330,6 +326,13 @@ } } ] + }, + "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" } } } diff --git a/docs/swagger/paths/v1_short-urls_shorten.json b/docs/swagger/paths/v1_short-urls_shorten.json index 90c3eda5..e1d98129 100644 --- a/docs/swagger/paths/v1_short-urls_shorten.json +++ b/docs/swagger/paths/v1_short-urls_shorten.json @@ -49,35 +49,33 @@ "application/json": { "schema": { "$ref": "../definitions/ShortUrl.json" + }, + "example": { + "longUrl": "https://github.com/shlinkio/shlink", + "shortUrl": "https://doma.in/abc123", + "shortCode": "abc123", + "dateCreated": "2016-08-21T20:34:16+02:00", + "visitsCount": 0, + "tags": [ + "games", + "tech" + ], + "meta": { + "validSince": "2017-01-21T00:00:00+02:00", + "validUntil": null, + "maxVisits": 100 + }, + "domain": null, + "title": null, + "crawlable": false } }, "text/plain": { "schema": { "type": "string" - } - } - }, - "examples": { - "application/json": { - "longUrl": "https://github.com/shlinkio/shlink", - "shortUrl": "https://doma.in/abc123", - "shortCode": "abc123", - "dateCreated": "2016-08-21T20:34:16+02:00", - "visitsCount": 0, - "tags": [ - "games", - "tech" - ], - "meta": { - "validSince": "2017-01-21T00:00:00+02:00", - "validUntil": null, - "maxVisits": 100 }, - "domain": null, - "title": null, - "crawlable": false - }, - "text/plain": "https://doma.in/abc123" + "example": "https://doma.in/abc123" + } } }, "400": { @@ -86,23 +84,21 @@ "application/problem+json": { "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" } }, "text/plain": { "schema": { "type": "string" - } + }, + "example": "INVALID_URL" } - }, - "examples": { - "application/problem+json": { - "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": "INVALID_URL" } }, "500": { @@ -118,13 +114,6 @@ "type": "string" } } - }, - "examples": { - "application/problem+json": { - "error": "INTERNAL_SERVER_ERROR", - "message": "Unexpected error occurred" - }, - "text/plain": "INTERNAL_SERVER_ERROR" } } } diff --git a/docs/swagger/paths/v1_short-urls_{shortCode}.json b/docs/swagger/paths/v1_short-urls_{shortCode}.json index e37df965..6fac0745 100644 --- a/docs/swagger/paths/v1_short-urls_{shortCode}.json +++ b/docs/swagger/paths/v1_short-urls_{shortCode}.json @@ -35,27 +35,25 @@ "application/json": { "schema": { "$ref": "../definitions/ShortUrl.json" - } - } - }, - "examples": { - "application/json": { - "shortCode": "12Kb3", - "shortUrl": "https://doma.in/12Kb3", - "longUrl": "https://shlink.io", - "dateCreated": "2016-05-01T20:34:16+02:00", - "visitsCount": 1029, - "tags": [ - "shlink" - ], - "meta": { - "validSince": "2017-01-21T00:00:00+02:00", - "validUntil": null, - "maxVisits": 100 }, - "domain": null, - "title": null, - "crawlable": false + "example": { + "shortCode": "12Kb3", + "shortUrl": "https://doma.in/12Kb3", + "longUrl": "https://shlink.io", + "dateCreated": "2016-05-01T20:34:16+02:00", + "visitsCount": 1029, + "tags": [ + "shlink" + ], + "meta": { + "validSince": "2017-01-21T00:00:00+02:00", + "validUntil": null, + "maxVisits": 100 + }, + "domain": null, + "title": null, + "crawlable": false + } } } }, @@ -129,27 +127,25 @@ "application/json": { "schema": { "$ref": "../definitions/ShortUrl.json" - } - } - }, - "examples": { - "application/json": { - "shortCode": "12Kb3", - "shortUrl": "https://doma.in/12Kb3", - "longUrl": "https://shlink.io", - "dateCreated": "2016-05-01T20:34:16+02:00", - "visitsCount": 1029, - "tags": [ - "shlink" - ], - "meta": { - "validSince": "2017-01-21T00:00:00+02:00", - "validUntil": null, - "maxVisits": 100 }, - "domain": null, - "title": "Shlink - The URL shortener", - "crawlable": false + "example": { + "shortCode": "12Kb3", + "shortUrl": "https://doma.in/12Kb3", + "longUrl": "https://shlink.io", + "dateCreated": "2016-05-01T20:34:16+02:00", + "visitsCount": 1029, + "tags": [ + "shlink" + ], + "meta": { + "validSince": "2017-01-21T00:00:00+02:00", + "validUntil": null, + "maxVisits": 100 + }, + "domain": null, + "title": "Shlink - The URL shortener", + "crawlable": false + } } } }, @@ -247,17 +243,39 @@ "content": { "application/problem+json": { "schema": { - "$ref": "../definitions/Error.json" + "allOf": [ + { + "$ref": "../definitions/Error.json" + }, + { + "type": "object", + "required": ["shortCode", "threshold"], + "properties": { + "shortCode": { + "type": "string", + "description": "The short code with which we tried to find the short URL to delete" + }, + "domain": { + "type": "string", + "description": "The domain with which we tried to find the short URL to delete" + }, + "threshold": { + "type": "number", + "description": "The amount of visits currently configured as threshold to allow deleting short UYRLs or not" + } + } + } + ] + }, + "example": { + "title": "Cannot delete short URL", + "type": "INVALID_SHORTCODE_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": { - "application/problem+json": { - "title": "Cannot delete short URL", - "type": "INVALID_SHORTCODE_DELETION", - "detail": "It is not possible to delete URL with short code \"abc123\" because it has reached more than \"15\" visits.", - "status": 422 - } } }, "404": { diff --git a/docs/swagger/paths/v1_short-urls_{shortCode}_tags.json b/docs/swagger/paths/v1_short-urls_{shortCode}_tags.json index 6ea642b0..3cad48ad 100644 --- a/docs/swagger/paths/v1_short-urls_{shortCode}_tags.json +++ b/docs/swagger/paths/v1_short-urls_{shortCode}_tags.json @@ -69,14 +69,6 @@ } } } - }, - "examples": { - "application/json": { - "tags": [ - "games", - "tech" - ] - } } }, "400": { diff --git a/docs/swagger/paths/v1_short-urls_{shortCode}_visits.json b/docs/swagger/paths/v1_short-urls_{shortCode}_visits.json index e5bbbe86..9fbe5433 100644 --- a/docs/swagger/paths/v1_short-urls_{shortCode}_visits.json +++ b/docs/swagger/paths/v1_short-urls_{shortCode}_visits.json @@ -97,49 +97,47 @@ } } } - } - } - }, - "examples": { - "application/json": { - "visits": { - "data": [ - { - "referer": "https://twitter.com", - "date": "2015-08-20T05:05:03+04:00", - "userAgent": "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0 Mozilla/5.0 (Macintosh; Intel Mac OS X x.y; rv:42.0) Gecko/20100101 Firefox/42.0", - "visitLocation": null, - "potentialBot": false - }, - { - "referer": "https://t.co", - "date": "2015-08-20T05:05:03+04:00", - "userAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36", - "visitLocation": { - "cityName": "Cupertino", - "countryCode": "US", - "countryName": "United States", - "latitude": 37.3042, - "longitude": -122.0946, - "regionName": "California", - "timezone": "America/Los_Angeles" + }, + "example": { + "visits": { + "data": [ + { + "referer": "https://twitter.com", + "date": "2015-08-20T05:05:03+04:00", + "userAgent": "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0 Mozilla/5.0 (Macintosh; Intel Mac OS X x.y; rv:42.0) Gecko/20100101 Firefox/42.0", + "visitLocation": null, + "potentialBot": false }, - "potentialBot": false - }, - { - "referer": null, - "date": "2015-08-20T05:05:03+04:00", - "userAgent": "some_web_crawler/1.4", - "visitLocation": null, - "potentialBot": true + { + "referer": "https://t.co", + "date": "2015-08-20T05:05:03+04:00", + "userAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36", + "visitLocation": { + "cityName": "Cupertino", + "countryCode": "US", + "countryName": "United States", + "latitude": 37.3042, + "longitude": -122.0946, + "regionName": "California", + "timezone": "America/Los_Angeles" + }, + "potentialBot": false + }, + { + "referer": null, + "date": "2015-08-20T05:05:03+04:00", + "userAgent": "some_web_crawler/1.4", + "visitLocation": null, + "potentialBot": true + } + ], + "pagination": { + "currentPage": 5, + "pagesCount": 12, + "itemsPerPage": 10, + "itemsInCurrentPage": 10, + "totalItems": 115 } - ], - "pagination": { - "currentPage": 5, - "pagesCount": 12, - "itemsPerPage": 10, - "itemsInCurrentPage": 10, - "totalItems": 115 } } } diff --git a/docs/swagger/paths/v1_tags.json b/docs/swagger/paths/v1_tags.json index 8c3ada73..d33d2b51 100644 --- a/docs/swagger/paths/v1_tags.json +++ b/docs/swagger/paths/v1_tags.json @@ -57,18 +57,42 @@ } } } - } - } - }, - "examples": { - "application/json": { - "tags": { - "data": [ - "games", - "php", - "shlink", - "tech" - ] + }, + "examples": { + "Without stats": { + "value": { + "tags": { + "data": [ + "games", + "php", + "shlink", + "tech" + ] + } + } + }, + "With stats": { + "value": { + "tags": { + "data": [ + "games", + "shlink" + ], + "stats": [ + { + "tag": "games", + "shortUrlsCount": 10, + "visitsCount": 521 + }, + { + "tag": "shlink", + "shortUrlsCount": 7, + "visitsCount": 1087 + } + ] + } + } + } } } } @@ -149,18 +173,6 @@ } } } - }, - "examples": { - "application/json": { - "tags": { - "data": [ - "games", - "php", - "shlink", - "tech" - ] - } - } } }, "500": { diff --git a/docs/swagger/paths/v2_domains.json b/docs/swagger/paths/v2_domains.json index d92677c1..da41ac3f 100644 --- a/docs/swagger/paths/v2_domains.json +++ b/docs/swagger/paths/v2_domains.json @@ -53,45 +53,43 @@ } } } - } - } - }, - "examples": { - "application/json": { - "domains": { - "data": [ - { - "domain": "example.com", - "isDefault": true, - "redirects": { - "baseUrlRedirect": "https://example.com/my-landing-page", - "regular404Redirect": null, - "invalidShortUrlRedirect": "https://example.com/invalid-url" - } - }, - { - "domain": "aaa.com", - "isDefault": false, - "redirects": { - "baseUrlRedirect": null, - "regular404Redirect": null, - "invalidShortUrlRedirect": null - } - }, - { - "domain": "bbb.com", - "isDefault": false, - "redirects": { - "baseUrlRedirect": null, - "regular404Redirect": null, - "invalidShortUrlRedirect": "https://example.com/invalid-url" + }, + "example": { + "domains": { + "data": [ + { + "domain": "example.com", + "isDefault": true, + "redirects": { + "baseUrlRedirect": "https://example.com/my-landing-page", + "regular404Redirect": null, + "invalidShortUrlRedirect": "https://example.com/invalid-url" + } + }, + { + "domain": "aaa.com", + "isDefault": false, + "redirects": { + "baseUrlRedirect": null, + "regular404Redirect": null, + "invalidShortUrlRedirect": null + } + }, + { + "domain": "bbb.com", + "isDefault": false, + "redirects": { + "baseUrlRedirect": null, + "regular404Redirect": null, + "invalidShortUrlRedirect": "https://example.com/invalid-url" + } } + ], + "defaultRedirects": { + "baseUrlRedirect": "https://somewhere.com", + "regular404Redirect": null, + "invalidShortUrlRedirect": null } - ], - "defaultRedirects": { - "baseUrlRedirect": "https://somewhere.com", - "regular404Redirect": null, - "invalidShortUrlRedirect": null } } } diff --git a/docs/swagger/paths/v2_domains_redirects.json b/docs/swagger/paths/v2_domains_redirects.json index 031e1d43..5eee7cd6 100644 --- a/docs/swagger/paths/v2_domains_redirects.json +++ b/docs/swagger/paths/v2_domains_redirects.json @@ -55,15 +55,13 @@ "$ref": "../definitions/NotFoundRedirects.json" } ] + }, + "example": { + "baseUrlRedirect": "https://example.com/my-landing-page", + "regular404Redirect": null, + "invalidShortUrlRedirect": "https://example.com/invalid-url" } } - }, - "examples": { - "application/json": { - "baseUrlRedirect": "https://example.com/my-landing-page", - "regular404Redirect": null, - "invalidShortUrlRedirect": "https://example.com/invalid-url" - } } }, "400": { diff --git a/docs/swagger/paths/v2_mercure-info.json b/docs/swagger/paths/v2_mercure-info.json index 24f7fb5f..b21322c3 100644 --- a/docs/swagger/paths/v2_mercure-info.json +++ b/docs/swagger/paths/v2_mercure-info.json @@ -23,15 +23,13 @@ "application/json": { "schema": { "$ref": "../definitions/MercureInfo.json" + }, + "example": { + "mercureHubUrl": "https://example.com/.well-known/mercure", + "jwt": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJTaGxpbmsiLCJpYXQiOjE1ODY2ODY3MzIsImV4cCI6MTU4Njk0NTkzMiwibWVyY3VyZSI6eyJzdWJzY3JpYmUiOltdfX0.P-519lgU7dFz0bbNlRG1CXyqugGbaHon4kw6fu4QBdQ", + "jwtExpiration": "2020-04-15T12:18:52+02:00" } } - }, - "examples": { - "application/json": { - "mercureHubUrl": "https://example.com/.well-known/mercure", - "jwt": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJTaGxpbmsiLCJpYXQiOjE1ODY2ODY3MzIsImV4cCI6MTU4Njk0NTkzMiwibWVyY3VyZSI6eyJzdWJzY3JpYmUiOltdfX0.P-519lgU7dFz0bbNlRG1CXyqugGbaHon4kw6fu4QBdQ", - "jwtExpiration": "2020-04-15T12:18:52+02:00" - } } }, "501": { @@ -40,16 +38,14 @@ "application/problem+json": { "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": { - "application/json": { - "title": "Mercure integration not configured", - "type": "MERCURE_NOT_CONFIGURED", - "detail": "This Shlink instance is not integrated with a mercure hub.", - "status": 501 - } } }, "500": { diff --git a/docs/swagger/paths/v2_tags_{tag}_visits.json b/docs/swagger/paths/v2_tags_{tag}_visits.json index df1242f6..a6701850 100644 --- a/docs/swagger/paths/v2_tags_{tag}_visits.json +++ b/docs/swagger/paths/v2_tags_{tag}_visits.json @@ -94,49 +94,47 @@ } } } - } - } - }, - "examples": { - "application/json": { - "visits": { - "data": [ - { - "referer": "https://twitter.com", - "date": "2015-08-20T05:05:03+04:00", - "userAgent": "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0 Mozilla/5.0 (Macintosh; Intel Mac OS X x.y; rv:42.0) Gecko/20100101 Firefox/42.0", - "visitLocation": null, - "potentialBot": false - }, - { - "referer": "https://t.co", - "date": "2015-08-20T05:05:03+04:00", - "userAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36", - "visitLocation": { - "cityName": "Cupertino", - "countryCode": "US", - "countryName": "United States", - "latitude": 37.3042, - "longitude": -122.0946, - "regionName": "California", - "timezone": "America/Los_Angeles" + }, + "example": { + "visits": { + "data": [ + { + "referer": "https://twitter.com", + "date": "2015-08-20T05:05:03+04:00", + "userAgent": "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0 Mozilla/5.0 (Macintosh; Intel Mac OS X x.y; rv:42.0) Gecko/20100101 Firefox/42.0", + "visitLocation": null, + "potentialBot": false }, - "potentialBot": false - }, - { - "referer": null, - "date": "2015-08-20T05:05:03+04:00", - "userAgent": "some_web_crawler/1.4", - "visitLocation": null, - "potentialBot": true + { + "referer": "https://t.co", + "date": "2015-08-20T05:05:03+04:00", + "userAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36", + "visitLocation": { + "cityName": "Cupertino", + "countryCode": "US", + "countryName": "United States", + "latitude": 37.3042, + "longitude": -122.0946, + "regionName": "California", + "timezone": "America/Los_Angeles" + }, + "potentialBot": false + }, + { + "referer": null, + "date": "2015-08-20T05:05:03+04:00", + "userAgent": "some_web_crawler/1.4", + "visitLocation": null, + "potentialBot": true + } + ], + "pagination": { + "currentPage": 5, + "pagesCount": 12, + "itemsPerPage": 10, + "itemsInCurrentPage": 10, + "totalItems": 115 } - ], - "pagination": { - "currentPage": 5, - "pagesCount": 12, - "itemsPerPage": 10, - "itemsInCurrentPage": 10, - "totalItems": 115 } } } diff --git a/docs/swagger/paths/v2_visits.json b/docs/swagger/paths/v2_visits.json index 3c712b1f..765d5eec 100644 --- a/docs/swagger/paths/v2_visits.json +++ b/docs/swagger/paths/v2_visits.json @@ -28,14 +28,12 @@ "$ref": "../definitions/VisitStats.json" } } - } - } - }, - "examples": { - "application/json": { - "visits": { - "visitsCount": 1569874, - "orphanVisitsCount": 71345 + }, + "example": { + "visits": { + "visitsCount": 1569874, + "orphanVisitsCount": 71345 + } } } } diff --git a/docs/swagger/paths/v2_visits_orphan.json b/docs/swagger/paths/v2_visits_orphan.json index ce52b197..e2a10285 100644 --- a/docs/swagger/paths/v2_visits_orphan.json +++ b/docs/swagger/paths/v2_visits_orphan.json @@ -85,55 +85,53 @@ } } } - } - } - }, - "examples": { - "application/json": { - "visits": { - "data": [ - { - "referer": "https://twitter.com", - "date": "2015-08-20T05:05:03+04:00", - "userAgent": "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0 Mozilla/5.0 (Macintosh; Intel Mac OS X x.y; rv:42.0) Gecko/20100101 Firefox/42.0", - "visitLocation": null, - "potentialBot": false, - "visitedUrl": "https://doma.in", - "type": "base_url" - }, - { - "referer": "https://t.co", - "date": "2015-08-20T05:05:03+04:00", - "userAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36", - "visitLocation": { - "cityName": "Cupertino", - "countryCode": "US", - "countryName": "United States", - "latitude": 37.3042, - "longitude": -122.0946, - "regionName": "California", - "timezone": "America/Los_Angeles" + }, + "example": { + "visits": { + "data": [ + { + "referer": "https://twitter.com", + "date": "2015-08-20T05:05:03+04:00", + "userAgent": "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0 Mozilla/5.0 (Macintosh; Intel Mac OS X x.y; rv:42.0) Gecko/20100101 Firefox/42.0", + "visitLocation": null, + "potentialBot": false, + "visitedUrl": "https://doma.in", + "type": "base_url" }, - "potentialBot": false, - "visitedUrl": "https://doma.in/foo", - "type": "invalid_short_url" - }, - { - "referer": null, - "date": "2015-08-20T05:05:03+04:00", - "userAgent": "some_web_crawler/1.4", - "visitLocation": null, - "potentialBot": true, - "visitedUrl": "https://doma.in/foo/bar/baz", - "type": "regular_404" + { + "referer": "https://t.co", + "date": "2015-08-20T05:05:03+04:00", + "userAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36", + "visitLocation": { + "cityName": "Cupertino", + "countryCode": "US", + "countryName": "United States", + "latitude": 37.3042, + "longitude": -122.0946, + "regionName": "California", + "timezone": "America/Los_Angeles" + }, + "potentialBot": false, + "visitedUrl": "https://doma.in/foo", + "type": "invalid_short_url" + }, + { + "referer": null, + "date": "2015-08-20T05:05:03+04:00", + "userAgent": "some_web_crawler/1.4", + "visitLocation": null, + "potentialBot": true, + "visitedUrl": "https://doma.in/foo/bar/baz", + "type": "regular_404" + } + ], + "pagination": { + "currentPage": 5, + "pagesCount": 12, + "itemsPerPage": 10, + "itemsInCurrentPage": 10, + "totalItems": 115 } - ], - "pagination": { - "currentPage": 5, - "pagesCount": 12, - "itemsPerPage": 10, - "itemsInCurrentPage": 10, - "totalItems": 115 } } } From 0fd941401b59268c6821054502af6a3ecee5242c Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Thu, 9 Dec 2021 18:28:47 +0100 Subject: [PATCH 32/66] Added extra examples for error responses in swagger docs --- .../examples/short-url-invalid-args.json | 9 ++ .../swagger/examples/short-url-not-found.json | 9 ++ docs/swagger/examples/tag-not-found.json | 9 ++ docs/swagger/paths/health.json | 2 +- docs/swagger/paths/v1_short-urls.json | 36 ++++++-- docs/swagger/paths/v1_short-urls_shorten.json | 2 +- .../paths/v1_short-urls_{shortCode}.json | 90 +++++++++++++++++-- .../paths/v1_short-urls_{shortCode}_tags.json | 2 +- .../v1_short-urls_{shortCode}_visits.json | 7 +- docs/swagger/paths/v1_tags.json | 40 ++++++++- docs/swagger/paths/v2_domains.json | 2 +- docs/swagger/paths/v2_domains_redirects.json | 9 +- docs/swagger/paths/v2_mercure-info.json | 2 +- docs/swagger/paths/v2_tags_{tag}_visits.json | 7 +- docs/swagger/paths/v2_visits.json | 2 +- docs/swagger/paths/v2_visits_orphan.json | 2 +- docs/swagger/paths/{shortCode}_qr-code.json | 11 +++ 17 files changed, 211 insertions(+), 30 deletions(-) create mode 100644 docs/swagger/examples/short-url-invalid-args.json create mode 100644 docs/swagger/examples/short-url-not-found.json create mode 100644 docs/swagger/examples/tag-not-found.json diff --git a/docs/swagger/examples/short-url-invalid-args.json b/docs/swagger/examples/short-url-invalid-args.json new file mode 100644 index 00000000..d85a5eed --- /dev/null +++ b/docs/swagger/examples/short-url-invalid-args.json @@ -0,0 +1,9 @@ +{ + "value": { + "title": "Invalid data", + "type": "INVALID_ARGUMENT", + "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.json new file mode 100644 index 00000000..74a5661c --- /dev/null +++ b/docs/swagger/examples/short-url-not-found.json @@ -0,0 +1,9 @@ +{ + "value": { + "detail":"No URL found with short code \"abc123\"", + "title":"Short URL not found", + "type": "INVALID_SHORTCODE", + "status": 404, + "shortCode": "abc123" + } +} diff --git a/docs/swagger/examples/tag-not-found.json b/docs/swagger/examples/tag-not-found.json new file mode 100644 index 00000000..46018121 --- /dev/null +++ b/docs/swagger/examples/tag-not-found.json @@ -0,0 +1,9 @@ +{ + "value": { + "detail": "Tag with name \"foo\" could not be found", + "title": "Tag not found", + "type": "TAG_NOT_FOUND", + "status": 404, + "tag": "foo" + } +} diff --git a/docs/swagger/paths/health.json b/docs/swagger/paths/health.json index c9cb300f..8dc5e7da 100644 --- a/docs/swagger/paths/health.json +++ b/docs/swagger/paths/health.json @@ -43,7 +43,7 @@ } } }, - "500": { + "default": { "description": "Unexpected error.", "content": { "application/json": { diff --git a/docs/swagger/paths/v1_short-urls.json b/docs/swagger/paths/v1_short-urls.json index ecef0285..4ef28bd2 100644 --- a/docs/swagger/paths/v1_short-urls.json +++ b/docs/swagger/paths/v1_short-urls.json @@ -187,7 +187,7 @@ } } }, - "500": { + "default": { "description": "Unexpected error.", "content": { "application/problem+json": { @@ -322,22 +322,42 @@ "customSlug": { "type": "string", "description": "Provided custom slug when the error type is INVALID_SLUG" + }, + "domain": {"type": "string", + "description": "The domain for which you were trying to create the new short URL" + } } } ] }, - "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": { + "Invalid arguments": { + "$ref": "../examples/short-url-invalid-args.json" + }, + "Invalid long URL": { + "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" + } + }, + "Non-unique slug": { + "value": { + "title": "Invalid custom slug", + "type": "INVALID_SLUG", + "detail": "Provided slug \"my-slug\" is already in use.", + "status": 400, + "customSlug": "my-slug" + } + } } } } }, - "500": { + "default": { "description": "Unexpected error.", "content": { "application/problem+json": { diff --git a/docs/swagger/paths/v1_short-urls_shorten.json b/docs/swagger/paths/v1_short-urls_shorten.json index e1d98129..722476bb 100644 --- a/docs/swagger/paths/v1_short-urls_shorten.json +++ b/docs/swagger/paths/v1_short-urls_shorten.json @@ -101,7 +101,7 @@ } } }, - "500": { + "default": { "description": "Unexpected error.", "content": { "application/problem+json": { diff --git a/docs/swagger/paths/v1_short-urls_{shortCode}.json b/docs/swagger/paths/v1_short-urls_{shortCode}.json index 6fac0745..eec1cec3 100644 --- a/docs/swagger/paths/v1_short-urls_{shortCode}.json +++ b/docs/swagger/paths/v1_short-urls_{shortCode}.json @@ -62,12 +62,35 @@ "content": { "application/problem+json": { "schema": { - "$ref": "../definitions/Error.json" + "allOf": [ + { + "$ref": "../definitions/Error.json" + }, + { + "type": "object", + "required": ["shortCode"], + "properties": { + "shortCode": { + "type": "string", + "description": "The short code with which we tried to find the short URL" + }, + "domain": { + "type": "string", + "description": "The domain with which we tried to find the short URL" + } + } + } + ] + }, + "examples": { + "Not found": { + "$ref": "../examples/short-url-not-found.json" + } } } } }, - "500": { + "default": { "description": "Unexpected error.", "content": { "application/problem+json": { @@ -178,21 +201,49 @@ } } ] + }, + "examples": { + "Invalid arguments": { + "$ref": "../examples/short-url-invalid-args.json" + } } } } }, "404": { - "description": "No short URL was found for provided short code.", + "description": "No URL was found for provided short code.", "content": { "application/problem+json": { "schema": { - "$ref": "../definitions/Error.json" + "allOf": [ + { + "$ref": "../definitions/Error.json" + }, + { + "type": "object", + "required": ["shortCode"], + "properties": { + "shortCode": { + "type": "string", + "description": "The short code with which we tried to find the short URL" + }, + "domain": { + "type": "string", + "description": "The domain with which we tried to find the short URL" + } + } + } + ] + }, + "examples": { + "Not found": { + "$ref": "../examples/short-url-not-found.json" + } } } } }, - "500": { + "default": { "description": "Unexpected error.", "content": { "application/problem+json": { @@ -279,16 +330,39 @@ } }, "404": { - "description": "No short URL was found for provided short code.", + "description": "No URL was found for provided short code.", "content": { "application/problem+json": { "schema": { - "$ref": "../definitions/Error.json" + "allOf": [ + { + "$ref": "../definitions/Error.json" + }, + { + "type": "object", + "required": ["shortCode"], + "properties": { + "shortCode": { + "type": "string", + "description": "The short code with which we tried to find the short URL" + }, + "domain": { + "type": "string", + "description": "The domain with which we tried to find the short URL" + } + } + } + ] + }, + "examples": { + "Not found": { + "$ref": "../examples/short-url-not-found.json" + } } } } }, - "500": { + "default": { "description": "Unexpected error.", "content": { "application/problem+json": { diff --git a/docs/swagger/paths/v1_short-urls_{shortCode}_tags.json b/docs/swagger/paths/v1_short-urls_{shortCode}_tags.json index 3cad48ad..645c6ef2 100644 --- a/docs/swagger/paths/v1_short-urls_{shortCode}_tags.json +++ b/docs/swagger/paths/v1_short-urls_{shortCode}_tags.json @@ -91,7 +91,7 @@ } } }, - "500": { + "default": { "description": "Unexpected error.", "content": { "application/json": { diff --git a/docs/swagger/paths/v1_short-urls_{shortCode}_visits.json b/docs/swagger/paths/v1_short-urls_{shortCode}_visits.json index 9fbe5433..08a93b68 100644 --- a/docs/swagger/paths/v1_short-urls_{shortCode}_visits.json +++ b/docs/swagger/paths/v1_short-urls_{shortCode}_visits.json @@ -149,11 +149,16 @@ "application/problem+json": { "schema": { "$ref": "../definitions/Error.json" + }, + "examples": { + "Short URL not found": { + "$ref": "../examples/short-url-not-found.json" + } } } } }, - "500": { + "default": { "description": "Unexpected error.", "content": { "application/problem+json": { diff --git a/docs/swagger/paths/v1_tags.json b/docs/swagger/paths/v1_tags.json index d33d2b51..12cdef81 100644 --- a/docs/swagger/paths/v1_tags.json +++ b/docs/swagger/paths/v1_tags.json @@ -97,7 +97,7 @@ } } }, - "500": { + "default": { "description": "Unexpected error.", "content": { "application/problem+json": { @@ -175,7 +175,7 @@ } } }, - "500": { + "default": { "description": "Unexpected error.", "content": { "application/problem+json": { @@ -240,6 +240,13 @@ "application/problem+json": { "schema": { "$ref": "../definitions/Error.json" + }, + "example": { + "title": "Invalid data", + "type": "INVALID_ARGUMENT", + "detail": "Provided data is not valid", + "status": 400, + "invalidElements": ["oldName", "newName"] } } } @@ -250,6 +257,12 @@ "application/problem+json": { "schema": { "$ref": "../definitions/Error.json" + }, + "example": { + "detail": "You are not allowed to rename tags", + "title": "Forbidden tag operation", + "type": "FORBIDDEN_OPERATION", + "status": 403 } } } @@ -260,6 +273,11 @@ "application/problem+json": { "schema": { "$ref": "../definitions/Error.json" + }, + "examples": { + "Tag not found": { + "$ref": "../examples/tag-not-found.json" + } } } } @@ -270,11 +288,19 @@ "application/problem+json": { "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" } } } }, - "500": { + "default": { "description": "Unexpected error.", "content": { "application/problem+json": { @@ -326,11 +352,17 @@ "application/problem+json": { "schema": { "$ref": "../definitions/Error.json" + }, + "example": { + "detail": "You are not allowed to delete tags", + "title": "Forbidden tag operation", + "type": "FORBIDDEN_OPERATION", + "status": 403 } } } }, - "500": { + "default": { "description": "Unexpected error.", "content": { "application/problem+json": { diff --git a/docs/swagger/paths/v2_domains.json b/docs/swagger/paths/v2_domains.json index da41ac3f..7568c64a 100644 --- a/docs/swagger/paths/v2_domains.json +++ b/docs/swagger/paths/v2_domains.json @@ -95,7 +95,7 @@ } } }, - "500": { + "default": { "description": "Unexpected error.", "content": { "application/problem+json": { diff --git a/docs/swagger/paths/v2_domains_redirects.json b/docs/swagger/paths/v2_domains_redirects.json index 5eee7cd6..d4d4338c 100644 --- a/docs/swagger/paths/v2_domains_redirects.json +++ b/docs/swagger/paths/v2_domains_redirects.json @@ -93,11 +93,18 @@ } } ] + }, + "example": { + "title": "Invalid data", + "type": "INVALID_ARGUMENT", + "detail": "Provided data is not valid", + "status": 400, + "invalidElements": ["domain", "invalidShortUrlRedirect"] } } } }, - "500": { + "default": { "description": "Unexpected error.", "content": { "application/problem+json": { diff --git a/docs/swagger/paths/v2_mercure-info.json b/docs/swagger/paths/v2_mercure-info.json index b21322c3..a341573f 100644 --- a/docs/swagger/paths/v2_mercure-info.json +++ b/docs/swagger/paths/v2_mercure-info.json @@ -48,7 +48,7 @@ } } }, - "500": { + "default": { "description": "Unexpected error.", "content": { "application/problem+json": { diff --git a/docs/swagger/paths/v2_tags_{tag}_visits.json b/docs/swagger/paths/v2_tags_{tag}_visits.json index a6701850..109cb1d0 100644 --- a/docs/swagger/paths/v2_tags_{tag}_visits.json +++ b/docs/swagger/paths/v2_tags_{tag}_visits.json @@ -146,11 +146,16 @@ "application/problem+json": { "schema": { "$ref": "../definitions/Error.json" + }, + "examples": { + "Tag not found": { + "$ref": "../examples/tag-not-found.json" + } } } } }, - "500": { + "default": { "description": "Unexpected error.", "content": { "application/problem+json": { diff --git a/docs/swagger/paths/v2_visits.json b/docs/swagger/paths/v2_visits.json index 765d5eec..ded6ac6b 100644 --- a/docs/swagger/paths/v2_visits.json +++ b/docs/swagger/paths/v2_visits.json @@ -38,7 +38,7 @@ } } }, - "500": { + "default": { "description": "Unexpected error.", "content": { "application/problem+json": { diff --git a/docs/swagger/paths/v2_visits_orphan.json b/docs/swagger/paths/v2_visits_orphan.json index e2a10285..03d56553 100644 --- a/docs/swagger/paths/v2_visits_orphan.json +++ b/docs/swagger/paths/v2_visits_orphan.json @@ -137,7 +137,7 @@ } } }, - "500": { + "default": { "description": "Unexpected error.", "content": { "application/problem+json": { diff --git a/docs/swagger/paths/{shortCode}_qr-code.json b/docs/swagger/paths/{shortCode}_qr-code.json index 04a88fd7..104860eb 100644 --- a/docs/swagger/paths/{shortCode}_qr-code.json +++ b/docs/swagger/paths/{shortCode}_qr-code.json @@ -60,6 +60,17 @@ "enum": ["L", "M", "Q", "H"], "default": "L" } + }, + { + "name": "roundBlockSize", + "in": "query", + "description": "Allows to disable block size rounding, which might reduce the readability of the QR code, but ensures no extra margin is added.", + "required": false, + "schema": { + "type": "string", + "enum": ["true", "false"], + "default": "false" + } } ], "responses": { From 15ce529c09c8af6d79be9d6830819d8bf7736f9d Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Thu, 9 Dec 2021 18:51:26 +0100 Subject: [PATCH 33/66] Added swagger validation to CI pipeline --- .github/workflows/ci.yml | 21 ++----------------- .gitignore | 1 + composer.json | 5 +++++ .../paths/{shortCode}_qr-code_{size}.json | 2 +- 4 files changed, 9 insertions(+), 20 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1db3f3eb..4645e0b1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,29 +8,12 @@ on: - develop jobs: - lint: - runs-on: ubuntu-20.04 - strategy: - matrix: - php-version: ['8.0'] - 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.8.1 - coverage: none - - run: composer install --no-interaction --prefer-dist - - run: composer cs - static-analysis: runs-on: ubuntu-20.04 strategy: matrix: php-version: ['8.0'] + command: ['cs', 'stan', 'swagger:validate'] steps: - name: Checkout code uses: actions/checkout@v2 @@ -42,7 +25,7 @@ jobs: extensions: openswoole-4.8.1 coverage: none - run: composer install --no-interaction --prefer-dist - - run: composer stan + - run: composer ${{ matrix.command }} unit-tests: runs-on: ubuntu-20.04 diff --git a/.gitignore b/.gitignore index 32942a29..933c25ee 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ docs/swagger-ui* docs/mercure.html docker-compose.override.yml .phpunit.result.cache +docs/swagger/swagger-inlined.json diff --git a/composer.json b/composer.json index 2926a1af..fedb863f 100644 --- a/composer.json +++ b/composer.json @@ -61,6 +61,7 @@ "symfony/string": "^5.4" }, "require-dev": { + "cebe/php-openapi": "^1.5", "devster/ubench": "^2.1", "dms/phpunit-arraysubset-asserts": "^0.3.0", "eaglewu/swoole-ide-helper": "dev-master", @@ -145,6 +146,8 @@ "@parallel test:unit:ci test:db:sqlite:ci", "@infect:ci" ], + "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" }, "scripts-descriptions": { @@ -170,6 +173,8 @@ "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", "infect:test": "Runs unit and db tests, then checks tests quality applying mutation testing", + "swagger:validate": "Validates the swagger docs, making sure they fulfil the spec", + "swagger:inline": "Inlines swagger docs in a single file", "clean:dev": "Deletes artifacts which are gitignored and could affect dev env" }, "config": { diff --git a/docs/swagger/paths/{shortCode}_qr-code_{size}.json b/docs/swagger/paths/{shortCode}_qr-code_{size}.json index fb5dd33e..54c5152e 100644 --- a/docs/swagger/paths/{shortCode}_qr-code_{size}.json +++ b/docs/swagger/paths/{shortCode}_qr-code_{size}.json @@ -21,7 +21,7 @@ "name": "size", "in": "path", "description": "The size of the image to be returned.", - "required": false, + "required": true, "schema": { "type": "integer", "minimum": 50, From 23c51a1d5f826933da24e4f0aec8a4d3c416858a Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Thu, 9 Dec 2021 18:52:27 +0100 Subject: [PATCH 34/66] Updated changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9aeca402..dcc88c9b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this ### Fixed * [#1206](https://github.com/shlinkio/shlink/issues/1206) Fixed debugging of the docker image, so that it does not run the commands with `-q` when the `SHELL_VERBOSITY` env var has been provided. +* [#1254](https://github.com/shlinkio/shlink/issues/1254) Fixed examples in swagger docs. ## [2.9.3] - 2021-11-15 From 181740c3e9acf0e49db6c69a7e86b74be975be0b Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Thu, 9 Dec 2021 18:55:17 +0100 Subject: [PATCH 35/66] Fixed typo in swagger docs --- docs/swagger/paths/v1_short-urls.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/swagger/paths/v1_short-urls.json b/docs/swagger/paths/v1_short-urls.json index 4ef28bd2..04afdd3a 100644 --- a/docs/swagger/paths/v1_short-urls.json +++ b/docs/swagger/paths/v1_short-urls.json @@ -323,9 +323,9 @@ "type": "string", "description": "Provided custom slug when the error type is INVALID_SLUG" }, - "domain": {"type": "string", + "domain": { + "type": "string", "description": "The domain for which you were trying to create the new short URL" - } } } From f7c0486101a2bc80402607d72c8153e67cbc3635 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Fri, 10 Dec 2021 12:52:36 +0100 Subject: [PATCH 36/66] Added swagger:validate to ci and ci:parallel commands --- composer.json | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/composer.json b/composer.json index fedb863f..abc6dc79 100644 --- a/composer.json +++ b/composer.json @@ -53,12 +53,12 @@ "shlinkio/shlink-importer": "dev-main#d099072 as 2.5", "shlinkio/shlink-installer": "dev-develop#7dd00fb as 6.3", "shlinkio/shlink-ip-geolocation": "^2.2", - "symfony/console": "^5.4", - "symfony/filesystem": "^5.4", - "symfony/lock": "^5.4", + "symfony/console": "^6.0 || ^5.4", + "symfony/filesystem": "^6.0 || ^5.4", + "symfony/lock": "^6.0 || ^5.4", "symfony/mercure": "^0.6", - "symfony/process": "^5.4", - "symfony/string": "^5.4" + "symfony/process": "^6.0 || ^5.4", + "symfony/string": "^6.0 || ^5.4" }, "require-dev": { "cebe/php-openapi": "^1.5", @@ -107,11 +107,12 @@ "ci": [ "@cs", "@stan", + "@swagger:validate", "@test:ci", "@infect:ci" ], "ci:parallel": [ - "@parallel cs stan test:unit:ci test:db:sqlite:ci test:db:mysql test:db:maria test:db:postgres test:db:ms", + "@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 test:api infect:ci:unit infect:ci:db" ], "cs": "phpcs", @@ -151,7 +152,7 @@ "clean:dev": "rm -f data/database.sqlite && rm -f config/params/generated_config.php" }, "scripts-descriptions": { - "ci": "Alias for \"cs\", \"stan\", \"test:ci\" and \"infect:ci\"", + "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", From 0786a962e79ac1f2a58d51b6a2fe088bb2b7894f Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Fri, 10 Dec 2021 13:42:33 +0100 Subject: [PATCH 37/66] Increased MIS to 83% --- composer.json | 4 + .../CLI/src/Command/Tag/ListTagsCommand.php | 3 +- .../test/Command/Api/ListKeysCommandTest.php | 27 ++++--- .../test/Command/Tag/ListTagsCommandTest.php | 18 +++-- module/Core/src/Entity/Visit.php | 2 +- .../ShortUrl/Spec/BelongsToApiKeyInlined.php | 2 +- .../ShortUrl/Spec/BelongsToDomainInlined.php | 2 +- module/Rest/src/Action/Tag/ListTagsAction.php | 2 +- .../src/Middleware/BodyParserMiddleware.php | 4 +- .../Request/DomainRedirectsRequestTest.php | 73 +++++++++++++++++++ 10 files changed, 114 insertions(+), 23 deletions(-) create mode 100644 module/Rest/test/Action/Domain/Request/DomainRedirectsRequestTest.php diff --git a/composer.json b/composer.json index abc6dc79..baa1b95c 100644 --- a/composer.json +++ b/composer.json @@ -147,6 +147,10 @@ "@parallel test:unit:ci test:db:sqlite:ci", "@infect:ci" ], + "infect:test:unit": [ + "@test:unit:ci", + "@infect:ci:unit" + ], "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/module/CLI/src/Command/Tag/ListTagsCommand.php b/module/CLI/src/Command/Tag/ListTagsCommand.php index 61d4e6e0..9eebe36f 100644 --- a/module/CLI/src/Command/Tag/ListTagsCommand.php +++ b/module/CLI/src/Command/Tag/ListTagsCommand.php @@ -45,7 +45,8 @@ class ListTagsCommand extends Command return map( $tags, - fn (TagInfo $tagInfo) => [(string) $tagInfo->tag(), $tagInfo->shortUrlsCount(), $tagInfo->visitsCount()], + static fn (TagInfo $tagInfo) => + [$tagInfo->tag()->__toString(), $tagInfo->shortUrlsCount(), $tagInfo->visitsCount()], ); } } diff --git a/module/CLI/test/Command/Api/ListKeysCommandTest.php b/module/CLI/test/Command/Api/ListKeysCommandTest.php index a124993f..68c1e844 100644 --- a/module/CLI/test/Command/Api/ListKeysCommandTest.php +++ b/module/CLI/test/Command/Api/ListKeysCommandTest.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace ShlinkioTest\Shlink\CLI\Command\Api; +use Cake\Chronos\Chronos; use PHPUnit\Framework\TestCase; use Prophecy\Prophecy\ObjectProphecy; use Shlinkio\Shlink\CLI\Command\Api\ListKeysCommand; @@ -45,19 +46,25 @@ class ListKeysCommandTest extends TestCase public function provideKeysAndOutputs(): iterable { + $dateInThePast = Chronos::createFromFormat('Y-m-d H:i:s', '2020-01-01 00:00:00'); + yield 'all keys' => [ - [$apiKey1 = ApiKey::create(), $apiKey2 = ApiKey::create(), $apiKey3 = ApiKey::create()], + [ + $apiKey1 = ApiKey::create()->disable(), + $apiKey2 = ApiKey::fromMeta(ApiKeyMeta::withExpirationDate($dateInThePast)), + $apiKey3 = ApiKey::create(), + ], false, <<commandTester->execute([]); $output = $this->commandTester->getDisplay(); - self::assertStringContainsString('| foo', $output); - self::assertStringContainsString('| bar', $output); - self::assertStringContainsString('| 10 ', $output); - self::assertStringContainsString('| 2 ', $output); - self::assertStringContainsString('| 7 ', $output); - self::assertStringContainsString('| 32 ', $output); + self::assertEquals( + <<shouldHaveBeenCalled(); } } diff --git a/module/Core/src/Entity/Visit.php b/module/Core/src/Entity/Visit.php index 8174e8be..c509bcc3 100644 --- a/module/Core/src/Entity/Visit.php +++ b/module/Core/src/Entity/Visit.php @@ -103,7 +103,7 @@ class Visit extends AbstractEntity implements JsonSerializable } try { - return (string) IpAddress::fromString($address)->getAnonymizedCopy(); + return IpAddress::fromString($address)->getAnonymizedCopy()->__toString(); } catch (InvalidArgumentException) { return null; } diff --git a/module/Core/src/ShortUrl/Spec/BelongsToApiKeyInlined.php b/module/Core/src/ShortUrl/Spec/BelongsToApiKeyInlined.php index 6b103058..809d19b7 100644 --- a/module/Core/src/ShortUrl/Spec/BelongsToApiKeyInlined.php +++ b/module/Core/src/ShortUrl/Spec/BelongsToApiKeyInlined.php @@ -17,6 +17,6 @@ class BelongsToApiKeyInlined implements Filter public function getFilter(QueryBuilder $qb, string $dqlAlias): string { // Parameters in this query need to be inlined, not bound, as we need to use it as sub-query later - return (string) $qb->expr()->eq('s.authorApiKey', '\'' . $this->apiKey->getId() . '\''); + return $qb->expr()->eq('s.authorApiKey', '\'' . $this->apiKey->getId() . '\'')->__toString(); } } diff --git a/module/Core/src/ShortUrl/Spec/BelongsToDomainInlined.php b/module/Core/src/ShortUrl/Spec/BelongsToDomainInlined.php index 4ce130b7..46fba689 100644 --- a/module/Core/src/ShortUrl/Spec/BelongsToDomainInlined.php +++ b/module/Core/src/ShortUrl/Spec/BelongsToDomainInlined.php @@ -16,6 +16,6 @@ class BelongsToDomainInlined implements Filter public function getFilter(QueryBuilder $qb, string $context): string { // Parameters in this query need to be inlined, not bound, as we need to use it as sub-query later - return (string) $qb->expr()->eq('s.domain', '\'' . $this->domainId . '\''); + return $qb->expr()->eq('s.domain', '\'' . $this->domainId . '\'')->__toString(); } } diff --git a/module/Rest/src/Action/Tag/ListTagsAction.php b/module/Rest/src/Action/Tag/ListTagsAction.php index 89371b71..3d34bd19 100644 --- a/module/Rest/src/Action/Tag/ListTagsAction.php +++ b/module/Rest/src/Action/Tag/ListTagsAction.php @@ -38,7 +38,7 @@ class ListTagsAction extends AbstractRestAction } $tagsInfo = $this->tagService->tagsInfo($apiKey); - $data = map($tagsInfo, fn (TagInfo $info) => (string) $info->tag()); + $data = map($tagsInfo, static fn (TagInfo $info) => $info->tag()->__toString()); return new JsonResponse([ 'tags' => [ diff --git a/module/Rest/src/Middleware/BodyParserMiddleware.php b/module/Rest/src/Middleware/BodyParserMiddleware.php index c7e99121..2711d900 100644 --- a/module/Rest/src/Middleware/BodyParserMiddleware.php +++ b/module/Rest/src/Middleware/BodyParserMiddleware.php @@ -54,7 +54,7 @@ class BodyParserMiddleware implements MiddlewareInterface, RequestMethodInterfac private function parseFromJson(Request $request): Request { - $rawBody = (string) $request->getBody(); + $rawBody = $request->getBody()->__toString(); if (empty($rawBody)) { return $request; } @@ -68,7 +68,7 @@ class BodyParserMiddleware implements MiddlewareInterface, RequestMethodInterfac */ private function parseFromUrlEncoded(Request $request): Request { - $rawBody = (string) $request->getBody(); + $rawBody = $request->getBody()->__toString(); if (empty($rawBody)) { return $request; } diff --git a/module/Rest/test/Action/Domain/Request/DomainRedirectsRequestTest.php b/module/Rest/test/Action/Domain/Request/DomainRedirectsRequestTest.php new file mode 100644 index 00000000..55828368 --- /dev/null +++ b/module/Rest/test/Action/Domain/Request/DomainRedirectsRequestTest.php @@ -0,0 +1,73 @@ +expectException(ValidationException::class); + DomainRedirectsRequest::fromRawData($data); + } + + public function provideInvalidData(): iterable + { + yield 'missing domain' => [[]]; + yield 'invalid domain' => [['domain' => 'foo:bar:baz']]; + } + + /** + * @test + * @dataProvider provideValidData + */ + public function isProperlyCastToNotFoundRedirects( + array $data, + ?NotFoundRedirectConfigInterface $defaults, + string $expectedAuthority, + ?string $expectedBaseUrlRedirect, + ?string $expectedRegular404Redirect, + ?string $expectedInvalidShortUrlRedirect, + ): void { + $request = DomainRedirectsRequest::fromRawData($data); + $notFound = $request->toNotFoundRedirects($defaults); + + self::assertEquals($expectedAuthority, $request->authority()); + self::assertEquals($expectedBaseUrlRedirect, $notFound->baseUrlRedirect()); + self::assertEquals($expectedRegular404Redirect, $notFound->regular404Redirect()); + self::assertEquals($expectedInvalidShortUrlRedirect, $notFound->invalidShortUrlRedirect()); + } + + public function provideValidData(): iterable + { + yield 'no values' => [['domain' => 'foo'], null, 'foo', null, null, null]; + yield 'some values' => [['domain' => 'foo', 'regular404Redirect' => 'bar'], null, 'foo', null, 'bar', null]; + yield 'fallbacks' => [ + ['domain' => 'domain', 'baseUrlRedirect' => 'bar'], + new NotFoundRedirectOptions(['regular404' => 'fallback', 'invalidShortUrl' => 'fallback2']), + 'domain', + 'bar', + 'fallback', + 'fallback2', + ]; + yield 'fallback ignored' => [ + ['domain' => 'domain', 'regular404Redirect' => 'bar', 'invalidShortUrlRedirect' => null], + new NotFoundRedirectOptions(['regular404' => 'fallback', 'invalidShortUrl' => 'fallback2']), + 'domain', + null, + 'bar', + null, + ]; + } +} From 3f3cf5e20edee042a9a38f3ce714b76a55bfbaed Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Fri, 10 Dec 2021 14:00:55 +0100 Subject: [PATCH 38/66] Explicitly required an MSI of 83 for unit tests --- composer.json | 2 +- .../Action/Visit/OrphanVisitsActionTest.php | 7 ++-- .../test/ApiKey/Model/RoleDefinitionTest.php | 32 +++++++++++++++++++ 3 files changed, 38 insertions(+), 3 deletions(-) create mode 100644 module/Rest/test/ApiKey/Model/RoleDefinitionTest.php diff --git a/composer.json b/composer.json index baa1b95c..bb9319c1 100644 --- a/composer.json +++ b/composer.json @@ -140,7 +140,7 @@ "test:db:ms": "DB_DRIVER=mssql composer test:db:sqlite", "test:api": "bin/test/run-api-tests.sh", "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:unit": "@infect:ci:base --coverage=build/coverage-unit --min-msi=83", "infect:ci:db": "@infect:ci:base --coverage=build/coverage-db --min-msi=95 --configuration=infection-db.json", "infect:ci": "@parallel infect:ci:unit infect:ci:db", "infect:test": [ diff --git a/module/Rest/test/Action/Visit/OrphanVisitsActionTest.php b/module/Rest/test/Action/Visit/OrphanVisitsActionTest.php index 9fec7e1f..43209e51 100644 --- a/module/Rest/test/Action/Visit/OrphanVisitsActionTest.php +++ b/module/Rest/test/Action/Visit/OrphanVisitsActionTest.php @@ -45,13 +45,16 @@ class OrphanVisitsActionTest extends TestCase $orphanVisits = $this->visitsHelper->orphanVisits(Argument::type(VisitsParams::class))->willReturn( new Paginator(new ArrayAdapter($visits)), ); + $visitsAmount = count($visits); $transform = $this->orphanVisitTransformer->transform(Argument::type(Visit::class))->willReturn([]); + /** @var JsonResponse $response */ $response = $this->action->handle(ServerRequestFactory::fromGlobals()); + $payload = $response->getPayload(); - self::assertInstanceOf(JsonResponse::class, $response); + self::assertCount($visitsAmount, $payload['visits']['data']); self::assertEquals(200, $response->getStatusCode()); $orphanVisits->shouldHaveBeenCalledOnce(); - $transform->shouldHaveBeenCalledTimes(count($visits)); + $transform->shouldHaveBeenCalledTimes($visitsAmount); } } diff --git a/module/Rest/test/ApiKey/Model/RoleDefinitionTest.php b/module/Rest/test/ApiKey/Model/RoleDefinitionTest.php new file mode 100644 index 00000000..8e6a58ad --- /dev/null +++ b/module/Rest/test/ApiKey/Model/RoleDefinitionTest.php @@ -0,0 +1,32 @@ +roleName()); + self::assertEquals([], $definition->meta()); + } + + /** @test */ + public function forDomainCreatesRoleDefinitionAsExpected(): void + { + $domain = Domain::withAuthority('foo.com')->setId('123'); + $definition = RoleDefinition::forDomain($domain); + + self::assertEquals(Role::DOMAIN_SPECIFIC, $definition->roleName()); + self::assertEquals(['domain_id' => '123', 'authority' => 'foo.com'], $definition->meta()); + } +} From bfea3f35f022a557974a8fb972de1e2c41f4d8e2 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Fri, 10 Dec 2021 14:01:58 +0100 Subject: [PATCH 39/66] Updated changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index dcc88c9b..263ae923 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this * [#1218](https://github.com/shlinkio/shlink/issues/1218) Updated to symfony/mercure 0.6. * [#1223](https://github.com/shlinkio/shlink/issues/1223) Updated to phpstan 1.0. * Added `domain` field to `DeleteShortUrlException` exception. +* [#1001](https://github.com/shlinkio/shlink/issues/1001) Increased required MSI to 83%. ### Deprecated * *Nothing* From 0d936425c2e7e7afe8735bd64ae59dd7bc7644cd Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Fri, 10 Dec 2021 16:24:38 +0100 Subject: [PATCH 40/66] Added new IS_HTTPS_ENABLED env var and deprecated USE_HTTPS --- CHANGELOG.md | 4 +++- config/autoload/url-shortener.global.php | 16 ++++++++++------ docker/README.md | 4 ++-- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 263ae923..24ef65b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,7 +31,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this * [#1001](https://github.com/shlinkio/shlink/issues/1001) Increased required MSI to 83%. ### Deprecated -* *Nothing* +* [#1260](https://github.com/shlinkio/shlink/issues/1260) Deprecated `USE_HTTPS` env var that was added in previous release, in favor of the new `IS_HTTPS_ENABLED`. + + The old one proved to be confusing and misleading, making people think it was used to actually enable HTTPS transparently, instead of its actual purpose, which is just telling Shlink it is being served with HTTPS. ### Removed * *Nothing* diff --git a/config/autoload/url-shortener.global.php b/config/autoload/url-shortener.global.php index aedab669..e14ceddb 100644 --- a/config/autoload/url-shortener.global.php +++ b/config/autoload/url-shortener.global.php @@ -8,13 +8,17 @@ use const Shlinkio\Shlink\DEFAULT_SHORT_CODES_LENGTH; use const Shlinkio\Shlink\MIN_SHORT_CODES_LENGTH; return (static function (): array { - $shortCodesLength = (int) env('DEFAULT_SHORT_CODES_LENGTH', DEFAULT_SHORT_CODES_LENGTH); - $shortCodesLength = $shortCodesLength < MIN_SHORT_CODES_LENGTH ? MIN_SHORT_CODES_LENGTH : $shortCodesLength; + $shortCodesLength = max( + (int) env('DEFAULT_SHORT_CODES_LENGTH', DEFAULT_SHORT_CODES_LENGTH), + MIN_SHORT_CODES_LENGTH, + ); $resolveSchema = static function (): string { - $useHttps = env('USE_HTTPS'); // Deprecated. For v3, set this to true by default, instead of null - if ($useHttps !== null) { - $boolUseHttps = (bool) $useHttps; - return $boolUseHttps ? 'https' : 'http'; + // Deprecated. For v3, IS_HTTPS_ENABLED should be true by default, instead of null +// return ((bool) env('IS_HTTPS_ENABLED', true)) ? 'https' : 'http'; + $isHttpsEnabled = env('IS_HTTPS_ENABLED', env('USE_HTTPS')); + if ($isHttpsEnabled !== null) { + $boolIsHttpsEnabled = (bool) $isHttpsEnabled; + return $boolIsHttpsEnabled ? 'https' : 'http'; } return env('SHORT_DOMAIN_SCHEMA', 'http'); diff --git a/docker/README.md b/docker/README.md index 7c9d3957..b7b92dcf 100644 --- a/docker/README.md +++ b/docker/README.md @@ -12,7 +12,7 @@ It exposes a shlink instance served with [openswoole](https://www.swoole.co.uk/) The most basic way to run Shlink's docker image is by providing these mandatory env vars. * `DEFAULT_DOMAIN`: The default short domain used for this shlink instance. For example **doma.in**. -* `USE_HTTPS`: Either **true** or **false**. +* `IS_HTTPS_ENABLED`: Either **true** or **false**. Tells if Shlink is being served with HTTPs or not. * `GEOLITE_LICENSE_KEY`: Your GeoLite2 license key. [Learn more](https://shlink.io/documentation/geolite-license-key/) about this. To run shlink on top of a local docker service, and using an internal SQLite database, do the following: @@ -22,7 +22,7 @@ docker run \ --name shlink \ -p 8080:8080 \ -e DEFAULT_DOMAIN=doma.in \ - -e USE_HTTPS=true \ + -e IS_HTTPS_ENABLED=true \ -e GEOLITE_LICENSE_KEY=kjh23ljkbndskj345 \ shlinkio/shlink:stable ``` From 6aebaa94afaba8e71d896d032fb046e9980e0d4a Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Fri, 10 Dec 2021 17:45:50 +0100 Subject: [PATCH 41/66] Added mutations to API tests --- .github/workflows/ci.yml | 6 ++-- composer.json | 15 ++++++---- config/test/bootstrap_api_tests.php | 3 +- config/test/test_config.global.php | 43 ++++++++++++++++++----------- infection-api.json | 23 +++++++++++++++ phpunit-api.xml | 3 +- 6 files changed, 66 insertions(+), 27 deletions(-) create mode 100644 infection-api.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4645e0b1..b24f9135 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -202,7 +202,7 @@ jobs: strategy: matrix: php-version: ['8.0', '8.1'] - test-group: ['unit', 'db'] + test-group: ['unit', 'db', 'api'] steps: - name: Checkout code uses: actions/checkout@v2 @@ -222,8 +222,8 @@ jobs: run: composer infect:ci:unit env: INFECTION_BADGE_API_KEY: ${{ secrets.INFECTION_BADGE_API_KEY }} - - if: ${{ matrix.test-group == 'db' }} - run: composer infect:ci:db + - if: ${{ matrix.test-group != 'unit' }} + run: composer infect:ci:${{ matrix.test-group }} upload-coverage: needs: diff --git a/composer.json b/composer.json index bb9319c1..615173c0 100644 --- a/composer.json +++ b/composer.json @@ -74,7 +74,7 @@ "phpunit/phpunit": "^9.5", "roave/security-advisories": "dev-master", "shlinkio/php-coding-standard": "~2.2.0", - "shlinkio/shlink-test-utils": "^2.4", + "shlinkio/shlink-test-utils": "^2.5", "symfony/var-dumper": "^6.0", "veewee/composer-run-parallel": "^1.1" }, @@ -113,7 +113,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 test:api infect:ci:unit infect:ci:db" + "@parallel infect:test:api infect:ci:unit infect:ci:db" ], "cs": "phpcs", "cs:fix": "phpcbf", @@ -130,7 +130,7 @@ ], "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:pretty": "@php vendor/bin/phpunit --order-by=random --colors=always --coverage-html build/coverage-unit-html", + "test:unit:pretty": "@php vendor/bin/phpunit --order-by=random --colors=always --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", "test:db:sqlite:ci": "@test:db:sqlite --coverage-php build/coverage-db.cov --coverage-xml=build/coverage-db/coverage-xml --log-junit=build/coverage-db/junit.xml", @@ -142,15 +142,20 @@ "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=83", "infect:ci:db": "@infect:ci:base --coverage=build/coverage-db --min-msi=95 --configuration=infection-db.json", - "infect:ci": "@parallel infect:ci:unit infect:ci:db", + "infect:ci:api": "@infect:ci:base --coverage=build/coverage-api --min-msi=80 --configuration=infection-api.json", + "infect:ci": "@parallel infect:ci:unit infect:ci:db infect:ci:api", "infect:test": [ - "@parallel test:unit:ci test:db:sqlite:ci", + "@parallel test:unit:ci test:db:sqlite:ci test:api", "@infect:ci" ], "infect:test:unit": [ "@test:unit:ci", "@infect:ci:unit" ], + "infect:test:api": [ + "@test:api", + "@infect:ci:api" + ], "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/config/test/bootstrap_api_tests.php b/config/test/bootstrap_api_tests.php index 8d22d029..52c9d4fb 100644 --- a/config/test/bootstrap_api_tests.php +++ b/config/test/bootstrap_api_tests.php @@ -20,8 +20,7 @@ $config = $container->get('config'); $em = $container->get(EntityManager::class); $httpClient = $container->get('shlink_test_api_client'); -// Start code coverage collecting on swoole process, and stop it when process shuts down -$httpClient->request('GET', sprintf('http://%s:%s/api-tests/start-coverage', SWOOLE_TESTING_HOST, SWOOLE_TESTING_PORT)); +// Dump code coverage when process shuts down register_shutdown_function(function () use ($httpClient): void { $httpClient->request( 'GET', diff --git a/config/test/test_config.global.php b/config/test/test_config.global.php index 68d1011c..d5e5476c 100644 --- a/config/test/test_config.global.php +++ b/config/test/test_config.global.php @@ -8,13 +8,16 @@ use GuzzleHttp\Client; use Laminas\ConfigAggregator\ConfigAggregator; use Laminas\Diactoros\Response\EmptyResponse; use Laminas\ServiceManager\Factory\InvokableFactory; -use Laminas\Stdlib\Glob; use Monolog\Handler\StreamHandler; use Monolog\Logger; use PHPUnit\Runner\Version; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Server\RequestHandlerInterface; use SebastianBergmann\CodeCoverage\CodeCoverage; use SebastianBergmann\CodeCoverage\Driver\Selector; 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; @@ -29,9 +32,8 @@ use const ShlinkioTest\Shlink\SWOOLE_TESTING_PORT; $isApiTest = env('TEST_ENV') === 'api'; if ($isApiTest) { $filter = new Filter(); - foreach (Glob::glob(__DIR__ . '/../../module/*/src') as $item) { - $filter->includeDirectory($item); - } + $filter->includeDirectory(__DIR__ . '/../../module/Core/src'); + $filter->includeDirectory(__DIR__ . '/../../module/Rest/src'); $coverage = new CodeCoverage((new Selector())->forLineCoverage($filter), $filter); } @@ -113,26 +115,17 @@ return [ ], 'routes' => !$isApiTest ? [] : [ - [ - 'name' => 'start_collecting_coverage', - 'path' => '/api-tests/start-coverage', - 'middleware' => middleware(static function () use (&$coverage) { - if ($coverage) { // @phpstan-ignore-line - $coverage->start('API tests'); - } - return new EmptyResponse(); - }), - 'allowed_methods' => ['GET'], - ], [ 'name' => 'dump_coverage', 'path' => '/api-tests/stop-coverage', 'middleware' => middleware(static function () use (&$coverage) { if ($coverage) { // @phpstan-ignore-line $basePath = __DIR__ . '/../../build/coverage-api'; - $coverage->stop(); + + // TODO Generate these coverages dynamically based on CLI options (new PHP())->process($coverage, $basePath . '.cov'); (new Xml(Version::getVersionString()))->process($coverage, $basePath . '/coverage-xml'); + (new Html())->process($coverage, $basePath . '/coverage-html'); } return new EmptyResponse(); @@ -141,6 +134,24 @@ return [ ], ], + 'middleware_pipeline' => !$isApiTest ? [] : [ + 'capture_code_coverage' => [ + 'middleware' => middleware(static function ( + ServerRequestInterface $req, + RequestHandlerInterface $handler, + ) use (&$coverage): ResponseInterface { + $coverage?->start($req->getHeaderLine('x-coverage-id')); + + try { + return $handler->handle($req); + } finally { + $coverage?->stop(); + } + }), + 'priority' => 9999, + ], + ], + 'mercure' => [ 'public_hub_url' => null, 'internal_hub_url' => null, diff --git a/infection-api.json b/infection-api.json new file mode 100644 index 00000000..398cd653 --- /dev/null +++ b/infection-api.json @@ -0,0 +1,23 @@ +{ + "source": { + "directories": [ + "module/*/src" + ] + }, + "timeout": 5, + "logs": { + "text": "build/infection-api/infection-log.txt", + "summary": "build/infection-api/summary-log.txt", + "debug": "build/infection-api/debug-log.txt" + }, + "tmpDir": "build/infection-api/temp", + "phpUnit": { + "configDir": "." + }, + "testFrameworkOptions": "--configuration=phpunit-api.xml", + "mutators": { + "@default": true, + "IdenticalEqual": false, + "NotIdenticalNotEqual": false + } +} diff --git a/phpunit-api.xml b/phpunit-api.xml index 38a53ca4..6dd527de 100644 --- a/phpunit-api.xml +++ b/phpunit-api.xml @@ -13,7 +13,8 @@ - ./module/*/src + ./module/Core/src + ./module/Rest/src From 064fef5d8a43ce78d5b36ba8eeb2349830c69ba9 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Fri, 10 Dec 2021 18:12:00 +0100 Subject: [PATCH 42/66] Added comment to explain why API tests coverage is generated the way it is --- config/test/test_config.global.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/config/test/test_config.global.php b/config/test/test_config.global.php index d5e5476c..ab7a910f 100644 --- a/config/test/test_config.global.php +++ b/config/test/test_config.global.php @@ -119,6 +119,8 @@ return [ 'name' => 'dump_coverage', 'path' => '/api-tests/stop-coverage', 'middleware' => middleware(static function () use (&$coverage) { + // 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'; From 87f6b192074e4059635718b874315d3726022b25 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Fri, 10 Dec 2021 18:12:46 +0100 Subject: [PATCH 43/66] Updated changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 24ef65b5..d0cbf02b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this The official docker image has also been updated to use PHP 8.1 by default. ### Changed +* [#844](https://github.com/shlinkio/shlink/issues/844) Added mutation checks to API tests. * [#1218](https://github.com/shlinkio/shlink/issues/1218) Updated to symfony/mercure 0.6. * [#1223](https://github.com/shlinkio/shlink/issues/1223) Updated to phpstan 1.0. * Added `domain` field to `DeleteShortUrlException` exception. From de2d87a6d958fa7e66c4d3a642d1c2f375cf7a22 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 11 Dec 2021 10:25:58 +0100 Subject: [PATCH 44/66] Unified jobs in ci pipeline as much as possible --- .github/workflows/ci.yml | 164 ++++-------------- bin/test/run-api-tests.sh | 1 + composer.json | 1 + config/autoload/entity-manager.local.php.dist | 2 +- config/test/test_config.global.php | 7 +- docker-compose.ci.yml | 2 +- docker-compose.override.yml.dist | 2 +- docker-compose.yml | 8 +- .../Config/SimplifiedConfigParserTest.php | 4 +- 9 files changed, 47 insertions(+), 144 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b24f9135..67b5b6b5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,107 +27,17 @@ jobs: - run: composer install --no-interaction --prefer-dist - run: composer ${{ matrix.command }} - unit-tests: - runs-on: ubuntu-20.04 - strategy: - matrix: - php-version: ['8.0', '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.8.1 - coverage: pcov - ini-values: pcov.directory=module - - run: composer install --no-interaction --prefer-dist - - run: composer test:unit:ci - - uses: actions/upload-artifact@v2 - if: ${{ matrix.php-version == '8.0' }} - with: - name: coverage-unit - path: | - build/coverage-unit - build/coverage-unit.cov - - db-tests-sqlite: - runs-on: ubuntu-20.04 - strategy: - matrix: - php-version: ['8.0', '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.8.1 - coverage: pcov - ini-values: pcov.directory=module - - run: composer install --no-interaction --prefer-dist - - run: composer test:db:sqlite:ci - - uses: actions/upload-artifact@v2 - if: ${{ matrix.php-version == '8.0' }} - with: - name: coverage-db - path: | - build/coverage-db - build/coverage-db.cov - - db-tests-mysql: - runs-on: ubuntu-20.04 - strategy: - matrix: - php-version: ['8.0', '8.1'] - steps: - - name: Checkout code - uses: actions/checkout@v2 - - name: Start database server - run: docker-compose -f docker-compose.yml -f docker-compose.ci.yml up -d shlink_db - - name: Use PHP - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php-version }} - tools: composer - extensions: openswoole-4.8.1 - coverage: none - - run: composer install --no-interaction --prefer-dist - - run: composer test:db:mysql - - db-tests-maria: - runs-on: ubuntu-20.04 - strategy: - matrix: - php-version: ['8.0', '8.1'] - steps: - - name: Checkout code - uses: actions/checkout@v2 - - name: Start database server - 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.8.1 - coverage: none - - run: composer install --no-interaction --prefer-dist - - run: composer test:db:maria - - db-tests-postgres: + tests: runs-on: ubuntu-20.04 strategy: matrix: php-version: ['8.0', '8.1'] + test-group: ['unit', 'api'] steps: - name: Checkout code uses: actions/checkout@v2 - name: Start 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: Use PHP uses: shivammathur/setup-php@v2 @@ -135,46 +45,35 @@ jobs: php-version: ${{ matrix.php-version }} tools: composer extensions: openswoole-4.8.1 - coverage: none + coverage: pcov + ini-values: pcov.directory=module - run: composer install --no-interaction --prefer-dist - - run: composer test:db:postgres + - run: composer test:${{ matrix.test-group }}:ci + - uses: actions/upload-artifact@v2 + if: ${{ matrix.php-version == '8.0' }} + with: + name: coverage-${{ matrix.test-group }} + path: | + build/coverage-${{ matrix.test-group }} + build/coverage-${{ matrix.test-group }}.cov - db-tests-ms: + db-tests: runs-on: ubuntu-20.04 strategy: matrix: php-version: ['8.0', '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 - run: docker-compose -f docker-compose.yml -f docker-compose.ci.yml up -d shlink_db_ms - - name: Use PHP - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php-version }} - tools: composer - extensions: openswoole-4.8.1, pdo_sqlsrv-5.10.0beta2 - coverage: none - - run: composer install --no-interaction --prefer-dist - - name: Create test database - run: docker-compose exec -T shlink_db_ms /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P 'Passw0rd!' -Q "CREATE DATABASE shlink_test;" - - run: composer test:db:ms - - api-tests: - runs-on: ubuntu-20.04 - strategy: - matrix: - php-version: ['8.0', '8.1'] - steps: - - name: Checkout code - uses: actions/checkout@v2 - - name: Start database server - run: docker-compose -f docker-compose.yml -f docker-compose.ci.yml up -d shlink_db_postgres + 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: @@ -184,20 +83,24 @@ jobs: coverage: pcov ini-values: pcov.directory=module - run: composer install --no-interaction --prefer-dist - - run: bin/test/run-api-tests.sh - - uses: actions/upload-artifact@v2 - if: ${{ matrix.php-version == '8.0' }} + - 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.0' && matrix.platform == 'sqlite:ci' }} with: - name: coverage-api + name: coverage-db path: | - build/coverage-api - build/coverage-api.cov + build/coverage-db + build/coverage-db.cov mutation-tests: needs: - - unit-tests - - db-tests-sqlite - - api-tests + - tests + - db-tests runs-on: ubuntu-20.04 strategy: matrix: @@ -227,9 +130,8 @@ jobs: upload-coverage: needs: - - unit-tests - - db-tests-sqlite - - api-tests + - tests + - db-tests runs-on: ubuntu-20.04 strategy: matrix: diff --git a/bin/test/run-api-tests.sh b/bin/test/run-api-tests.sh index dbd87a84..3e8530b6 100755 --- a/bin/test/run-api-tests.sh +++ b/bin/test/run-api-tests.sh @@ -2,6 +2,7 @@ export APP_ENV=test export DB_DRIVER=postgres export TEST_ENV=api +export GENERATE_COVERAGE=${GENERATE_COVERAGE:-"no"} rm -rf data/log/api-tests diff --git a/composer.json b/composer.json index 615173c0..6364443a 100644 --- a/composer.json +++ b/composer.json @@ -139,6 +139,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:ci": "GENERATE_COVERAGE=yes composer test:api", "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=83", "infect:ci:db": "@infect:ci:base --coverage=build/coverage-db --min-msi=95 --configuration=infection-db.json", diff --git a/config/autoload/entity-manager.local.php.dist b/config/autoload/entity-manager.local.php.dist index c4d2b921..ef5cabf8 100644 --- a/config/autoload/entity-manager.local.php.dist +++ b/config/autoload/entity-manager.local.php.dist @@ -9,7 +9,7 @@ return [ 'user' => 'root', 'password' => 'root', 'driver' => 'pdo_mysql', - 'host' => 'shlink_db', + 'host' => 'shlink_db_mysql', 'dbname' => 'shlink', 'charset' => 'utf8', ], diff --git a/config/test/test_config.global.php b/config/test/test_config.global.php index ab7a910f..0898c732 100644 --- a/config/test/test_config.global.php +++ b/config/test/test_config.global.php @@ -30,7 +30,8 @@ use const ShlinkioTest\Shlink\SWOOLE_TESTING_HOST; use const ShlinkioTest\Shlink\SWOOLE_TESTING_PORT; $isApiTest = env('TEST_ENV') === 'api'; -if ($isApiTest) { +$generateCoverage = env('GENERATE_COVERAGE') === 'yes'; +if ($isApiTest && $generateCoverage) { $filter = new Filter(); $filter->includeDirectory(__DIR__ . '/../../module/Core/src'); $filter->includeDirectory(__DIR__ . '/../../module/Rest/src'); @@ -40,7 +41,6 @@ if ($isApiTest) { $buildDbConnection = static function (): array { $driver = env('DB_DRIVER', 'sqlite'); $isCi = env('CI', false); - $getMysqlHost = static fn (string $driver) => sprintf('shlink_db%s', $driver === 'mysql' ? '' : '_maria'); $getCiMysqlPort = static fn (string $driver) => $driver === 'mysql' ? '3307' : '3308'; return match ($driver) { @@ -66,7 +66,7 @@ $buildDbConnection = static function (): array { ], default => [ // mysql and maria 'driver' => 'pdo_mysql', - 'host' => $isCi ? '127.0.0.1' : $getMysqlHost($driver), + 'host' => $isCi ? '127.0.0.1' : sprintf('shlink_db_%s', $driver), 'port' => $isCi ? $getCiMysqlPort($driver) : '3306', 'user' => 'root', 'password' => 'root', @@ -124,7 +124,6 @@ return [ if ($coverage) { // @phpstan-ignore-line $basePath = __DIR__ . '/../../build/coverage-api'; - // TODO Generate these coverages dynamically based on CLI options (new PHP())->process($coverage, $basePath . '.cov'); (new Xml(Version::getVersionString()))->process($coverage, $basePath . '/coverage-xml'); (new Html())->process($coverage, $basePath . '/coverage-html'); diff --git a/docker-compose.ci.yml b/docker-compose.ci.yml index 3783fef2..f4235dcb 100644 --- a/docker-compose.ci.yml +++ b/docker-compose.ci.yml @@ -1,7 +1,7 @@ version: '3' services: - shlink_db: + shlink_db_mysql: environment: MYSQL_DATABASE: shlink_test diff --git a/docker-compose.override.yml.dist b/docker-compose.override.yml.dist index ea0bee84..990d1b5d 100644 --- a/docker-compose.override.yml.dist +++ b/docker-compose.override.yml.dist @@ -13,7 +13,7 @@ services: - /etc/passwd:/etc/passwd:ro - /etc/group:/etc/group:ro - shlink_db: + shlink_db_mysql: user: 1000:1000 volumes: - /etc/passwd:/etc/passwd:ro diff --git a/docker-compose.yml b/docker-compose.yml index 8ea38d98..d9dd776c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -22,7 +22,7 @@ services: - ./:/home/shlink/www - ./data/infra/php.ini:/usr/local/etc/php/php.ini links: - - shlink_db + - shlink_db_mysql - shlink_db_postgres - shlink_db_maria - shlink_db_ms @@ -57,7 +57,7 @@ services: - ./:/home/shlink - ./data/infra/php.ini:/usr/local/etc/php/php.ini links: - - shlink_db + - shlink_db_mysql - shlink_db_postgres - shlink_db_maria - shlink_db_ms @@ -69,8 +69,8 @@ services: extra_hosts: - 'host.docker.internal:host-gateway' - shlink_db: - container_name: shlink_db + shlink_db_mysql: + container_name: shlink_db_mysql image: mysql:5.7 ports: - "3307:3306" diff --git a/module/Core/test/Config/SimplifiedConfigParserTest.php b/module/Core/test/Config/SimplifiedConfigParserTest.php index f4e5c8f0..48d41c00 100644 --- a/module/Core/test/Config/SimplifiedConfigParserTest.php +++ b/module/Core/test/Config/SimplifiedConfigParserTest.php @@ -29,7 +29,7 @@ class SimplifiedConfigParserTest extends TestCase 'entity_manager' => [ 'connection' => [ 'driver' => 'mysql', - 'host' => 'shlink_db', + 'host' => 'shlink_db_mysql', 'port' => '3306', ], ], @@ -78,7 +78,7 @@ class SimplifiedConfigParserTest extends TestCase 'entity_manager' => [ 'connection' => [ 'driver' => 'mysql', - 'host' => 'shlink_db', + 'host' => 'shlink_db_mysql', 'dbname' => 'shlink', 'user' => 'foo', 'password' => 'bar', From 7d7c0011bbf14727f440e5917acca5ce6f131932 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 11 Dec 2021 10:33:00 +0100 Subject: [PATCH 45/66] Fixed references to test:api and test:api:ci inside composer.json and added missing driver for MS SQL --- .github/workflows/ci.yml | 2 +- composer.json | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 67b5b6b5..051be99f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -79,7 +79,7 @@ jobs: with: php-version: ${{ matrix.php-version }} tools: composer - extensions: openswoole-4.8.1 + extensions: openswoole-4.8.1, pdo_sqlsrv-5.10.0beta2 coverage: pcov ini-values: pcov.directory=module - run: composer install --no-interaction --prefer-dist diff --git a/composer.json b/composer.json index 6364443a..b40d24ba 100644 --- a/composer.json +++ b/composer.json @@ -126,7 +126,7 @@ "test:ci": [ "@test:unit:ci", "@test:db", - "@test:api" + "@test:api: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", @@ -146,7 +146,7 @@ "infect:ci:api": "@infect:ci:base --coverage=build/coverage-api --min-msi=80 --configuration=infection-api.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", + "@parallel test:unit:ci test:db:sqlite:ci test:api:ci", "@infect:ci" ], "infect:test:unit": [ @@ -154,7 +154,7 @@ "@infect:ci:unit" ], "infect:test:api": [ - "@test:api", + "@test:api:ci", "@infect:ci:api" ], "swagger:validate": "php-openapi validate docs/swagger/swagger.json", @@ -171,6 +171,7 @@ "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", "test:db": "Runs database test suites on a SQLite, MySQL, MariaDB, PostgreSQL and MsSQL", "test:db:sqlite": "Runs database test suites on a SQLite database", "test:db:sqlite:ci": "Runs database test suites on a SQLite database, generating all needed reports and logs for CI envs", @@ -179,7 +180,7 @@ "test:db:postgres": "Runs database test suites on a PostgreSQL database", "test:db:ms": "Runs database test suites on a Miscrosoft SQL Server database", "test:api": "Runs API test suites", - "test:unit:pretty": "Runs unit test suites and generates an HTML code coverage report", + "test:api:ci": "Runs API test suites, and generates code coverage reports", "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", From 453842246f86303c4764761d423b4249a69f2597 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 11 Dec 2021 11:30:03 +0100 Subject: [PATCH 46/66] Ensured docker publish is run under ubuntu 20.04 --- .github/workflows/docker-image-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker-image-build.yml b/.github/workflows/docker-image-build.yml index ebdd1d05..a4f47026 100644 --- a/.github/workflows/docker-image-build.yml +++ b/.github/workflows/docker-image-build.yml @@ -9,7 +9,7 @@ on: jobs: build: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - name: Checkout code uses: actions/checkout@v2 From 05332e060609050fc20afd5ceb294d6f8ae58dc2 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 11 Dec 2021 11:40:59 +0100 Subject: [PATCH 47/66] Created workflow to publish swagger specs --- .github/workflows/publish-swagger-spec.yml | 50 ++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 .github/workflows/publish-swagger-spec.yml diff --git a/.github/workflows/publish-swagger-spec.yml b/.github/workflows/publish-swagger-spec.yml new file mode 100644 index 00000000..e310aa82 --- /dev/null +++ b/.github/workflows/publish-swagger-spec.yml @@ -0,0 +1,50 @@ +name: Publish swagger spec + +on: + workflow_dispatch: + inputs: + version: + description: The version to generate + required: true + push: + tags: + - 'v*' + +jobs: + build: + runs-on: ubuntu-20.04 + strategy: + matrix: + php-version: ['8.0'] + steps: + - name: Checkout code + uses: actions/checkout@v2 + - name: Determine version - from input + if: ${{ github.event.inputs.project != '' }} + id: determine_version + run: echo "::set-output name=version::$(echo ${{ github.event.inputs.project }})" + shell: bash + - name: Determine version - from env + if: ${{ github.event.inputs.project == '' }} + id: determine_version + run: echo "::set-output name=version::$(echo ${GITHUB_REF#refs/tags/})" + shell: bash + - name: Use PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-version }} + tools: composer + extensions: openswoole-4.8.1 + coverage: none + - run: composer install --no-interaction --prefer-dist + - run: composer swagger:inline + - run: mkdir ${{ steps.determine_version.outputs.version }} + - run: mv docs/swagger/swagger-inline.json ${{ steps.determine_version.outputs.version }}/oas.json + - name: Publish spec + uses: JamesIves/github-pages-deploy-action@4.1.7 + with: + repository-name: 'shlinkio/shlink-open-api-specs' + branch: main + folder: ${{ steps.determine_version.outputs.version }} + target-folder: specs + clean: false From dad58b76101773d7f8c27344d23844d93ae4cda0 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 11 Dec 2021 11:53:18 +0100 Subject: [PATCH 48/66] Disabled env step on publis-swagger workflow --- .github/workflows/publish-swagger-spec.yml | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/.github/workflows/publish-swagger-spec.yml b/.github/workflows/publish-swagger-spec.yml index e310aa82..95a6c147 100644 --- a/.github/workflows/publish-swagger-spec.yml +++ b/.github/workflows/publish-swagger-spec.yml @@ -6,9 +6,9 @@ on: version: description: The version to generate required: true - push: - tags: - - 'v*' +# push: +# tags: +# - 'v*' jobs: build: @@ -20,15 +20,13 @@ jobs: - name: Checkout code uses: actions/checkout@v2 - name: Determine version - from input - if: ${{ github.event.inputs.project != '' }} id: determine_version - run: echo "::set-output name=version::$(echo ${{ github.event.inputs.project }})" - shell: bash - - name: Determine version - from env - if: ${{ github.event.inputs.project == '' }} - id: determine_version - run: echo "::set-output name=version::$(echo ${GITHUB_REF#refs/tags/})" + run: echo "::set-output name=version::${{ github.event.inputs.project }}" shell: bash +# - name: Determine version - from env +# id: determine_version +# run: echo "::set-output name=version::${GITHUB_REF#refs/tags/}" +# shell: bash - name: Use PHP uses: shivammathur/setup-php@v2 with: From 5c114b584d0ea896255e28acb29c8d9e71327797 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 11 Dec 2021 12:11:22 +0100 Subject: [PATCH 49/66] 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 95a6c147..9b6a3c6c 100644 --- a/.github/workflows/publish-swagger-spec.yml +++ b/.github/workflows/publish-swagger-spec.yml @@ -21,7 +21,7 @@ jobs: uses: actions/checkout@v2 - name: Determine version - from input id: determine_version - run: echo "::set-output name=version::${{ github.event.inputs.project }}" + run: echo "::set-output name=version::${{ github.event.inputs.version }}" shell: bash # - name: Determine version - from env # id: determine_version From 295de5be8ef83fc635a36f5a17aa1bb403a057b5 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 11 Dec 2021 12:18:55 +0100 Subject: [PATCH 50/66] Changed how version is determined --- .github/workflows/publish-swagger-spec.yml | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/.github/workflows/publish-swagger-spec.yml b/.github/workflows/publish-swagger-spec.yml index 9b6a3c6c..c6147801 100644 --- a/.github/workflows/publish-swagger-spec.yml +++ b/.github/workflows/publish-swagger-spec.yml @@ -19,11 +19,9 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v2 - - name: Determine version - from input - id: determine_version - run: echo "::set-output name=version::${{ github.event.inputs.version }}" - shell: bash -# - name: Determine version - from env + - name: Determine version + run: echo ${{ github.event.inputs.version }} +# - name: Determine version # id: determine_version # run: echo "::set-output name=version::${GITHUB_REF#refs/tags/}" # shell: bash @@ -36,13 +34,16 @@ jobs: coverage: none - run: composer install --no-interaction --prefer-dist - run: composer swagger:inline - - run: mkdir ${{ steps.determine_version.outputs.version }} - - run: mv docs/swagger/swagger-inline.json ${{ steps.determine_version.outputs.version }}/oas.json +# - run: mkdir ${{ steps.determine_version.outputs.version }} + - run: mkdir ${{ github.event.inputs.version }} +# - run: mv docs/swagger/swagger-inline.json ${{ steps.determine_version.outputs.version }}/oas.json + - run: mv docs/swagger/swagger-inline.json${{ github.event.inputs.version }}/oas.json - name: Publish spec uses: JamesIves/github-pages-deploy-action@4.1.7 with: repository-name: 'shlinkio/shlink-open-api-specs' branch: main - folder: ${{ steps.determine_version.outputs.version }} +# folder: ${{ steps.determine_version.outputs.version }} + folder: ${{ github.event.inputs.version }} target-folder: specs clean: false From 8a93922da0b78294fa2051944f88e786f7f969fa Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 11 Dec 2021 12:27:45 +0100 Subject: [PATCH 51/66] Added missing space in mv command --- .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 c6147801..dd6dd845 100644 --- a/.github/workflows/publish-swagger-spec.yml +++ b/.github/workflows/publish-swagger-spec.yml @@ -37,7 +37,7 @@ jobs: # - run: mkdir ${{ steps.determine_version.outputs.version }} - run: mkdir ${{ github.event.inputs.version }} # - run: mv docs/swagger/swagger-inline.json ${{ steps.determine_version.outputs.version }}/oas.json - - run: mv docs/swagger/swagger-inline.json${{ github.event.inputs.version }}/oas.json + - run: mv docs/swagger/swagger-inline.json ${{ github.event.inputs.version }}/oas.json - name: Publish spec uses: JamesIves/github-pages-deploy-action@4.1.7 with: From 5a7f0ad34038bd8ffaf620473271a5ba8a5d7885 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 11 Dec 2021 12:35:34 +0100 Subject: [PATCH 52/66] Fixed another typo... --- .github/workflows/publish-swagger-spec.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/publish-swagger-spec.yml b/.github/workflows/publish-swagger-spec.yml index dd6dd845..9c250e08 100644 --- a/.github/workflows/publish-swagger-spec.yml +++ b/.github/workflows/publish-swagger-spec.yml @@ -19,8 +19,6 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v2 - - name: Determine version - run: echo ${{ github.event.inputs.version }} # - name: Determine version # id: determine_version # run: echo "::set-output name=version::${GITHUB_REF#refs/tags/}" @@ -37,7 +35,7 @@ jobs: # - run: mkdir ${{ steps.determine_version.outputs.version }} - run: mkdir ${{ github.event.inputs.version }} # - run: mv docs/swagger/swagger-inline.json ${{ steps.determine_version.outputs.version }}/oas.json - - run: mv docs/swagger/swagger-inline.json ${{ github.event.inputs.version }}/oas.json + - run: mv docs/swagger/swagger-inlined.json ${{ github.event.inputs.version }}/oas.json - name: Publish spec uses: JamesIves/github-pages-deploy-action@4.1.7 with: From 5bf25c7ecab8f3bf865c63b02d7a3a227d6158ee Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 11 Dec 2021 12:55:50 +0100 Subject: [PATCH 53/66] Added custom token for swagger publishing --- .github/workflows/publish-swagger-spec.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/publish-swagger-spec.yml b/.github/workflows/publish-swagger-spec.yml index 9c250e08..14f7c798 100644 --- a/.github/workflows/publish-swagger-spec.yml +++ b/.github/workflows/publish-swagger-spec.yml @@ -39,6 +39,7 @@ jobs: - name: Publish spec uses: JamesIves/github-pages-deploy-action@4.1.7 with: + token: ${{ secrets.OAS_PUBLISH_TOKEN }} repository-name: 'shlinkio/shlink-open-api-specs' branch: main # folder: ${{ steps.determine_version.outputs.version }} From 1b8bc9f0ffe807a5038a5303a5f8d53c02a902e0 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 11 Dec 2021 13:04:45 +0100 Subject: [PATCH 54/66] Ensured version subfolder is preserved when publishing swagger spec --- .github/workflows/publish-swagger-spec.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/publish-swagger-spec.yml b/.github/workflows/publish-swagger-spec.yml index 14f7c798..b1afc436 100644 --- a/.github/workflows/publish-swagger-spec.yml +++ b/.github/workflows/publish-swagger-spec.yml @@ -44,5 +44,6 @@ jobs: branch: main # folder: ${{ steps.determine_version.outputs.version }} folder: ${{ github.event.inputs.version }} - target-folder: specs +# target-folder: specs/${{ github.event.inputs.version }} + target-folder: specs/${{ steps.determine_version.outputs.version }} clean: false From c48a3a24f7dff3bab488ed80e7350e288ce2bffa Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 11 Dec 2021 13:09:39 +0100 Subject: [PATCH 55/66] Fix yet another typo in pipeline --- .github/workflows/publish-swagger-spec.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/publish-swagger-spec.yml b/.github/workflows/publish-swagger-spec.yml index b1afc436..6a7e7d5d 100644 --- a/.github/workflows/publish-swagger-spec.yml +++ b/.github/workflows/publish-swagger-spec.yml @@ -44,6 +44,6 @@ jobs: branch: main # folder: ${{ steps.determine_version.outputs.version }} folder: ${{ github.event.inputs.version }} -# target-folder: specs/${{ github.event.inputs.version }} - target-folder: specs/${{ steps.determine_version.outputs.version }} +# target-folder: specs/${{ steps.determine_version.outputs.version }} + target-folder: specs/${{ github.event.inputs.version }} clean: false From ec11155c9c74973c2c64815aa06f58e0fae88eb2 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 11 Dec 2021 13:17:45 +0100 Subject: [PATCH 56/66] Updated publish swagger workflow to be triggered for tags --- .github/workflows/publish-swagger-spec.yml | 31 ++++++++-------------- 1 file changed, 11 insertions(+), 20 deletions(-) diff --git a/.github/workflows/publish-swagger-spec.yml b/.github/workflows/publish-swagger-spec.yml index 6a7e7d5d..3264fad9 100644 --- a/.github/workflows/publish-swagger-spec.yml +++ b/.github/workflows/publish-swagger-spec.yml @@ -1,14 +1,9 @@ name: Publish swagger spec on: - workflow_dispatch: - inputs: - version: - description: The version to generate - required: true -# push: -# tags: -# - 'v*' + push: + tags: + - 'v*' jobs: build: @@ -19,10 +14,10 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v2 -# - name: Determine version -# id: determine_version -# run: echo "::set-output name=version::${GITHUB_REF#refs/tags/}" -# shell: bash + - name: Determine version + id: determine_version + run: echo "::set-output name=version::${GITHUB_REF#refs/tags/}" + shell: bash - name: Use PHP uses: shivammathur/setup-php@v2 with: @@ -32,18 +27,14 @@ jobs: coverage: none - run: composer install --no-interaction --prefer-dist - run: composer swagger:inline -# - run: mkdir ${{ steps.determine_version.outputs.version }} - - run: mkdir ${{ github.event.inputs.version }} -# - run: mv docs/swagger/swagger-inline.json ${{ steps.determine_version.outputs.version }}/oas.json - - run: mv docs/swagger/swagger-inlined.json ${{ github.event.inputs.version }}/oas.json + - run: mkdir ${{ steps.determine_version.outputs.version }} + - run: mv docs/swagger/swagger-inline.json ${{ steps.determine_version.outputs.version }}/oas.json - name: Publish spec uses: JamesIves/github-pages-deploy-action@4.1.7 with: token: ${{ secrets.OAS_PUBLISH_TOKEN }} repository-name: 'shlinkio/shlink-open-api-specs' branch: main -# folder: ${{ steps.determine_version.outputs.version }} - folder: ${{ github.event.inputs.version }} -# target-folder: specs/${{ steps.determine_version.outputs.version }} - target-folder: specs/${{ github.event.inputs.version }} + folder: ${{ steps.determine_version.outputs.version }} + target-folder: specs/${{ steps.determine_version.outputs.version }} clean: false From 69f4daa9d2a009cc95a769ebf82d79eff9adb740 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 11 Dec 2021 16:19:38 +0100 Subject: [PATCH 57/66] Added dev container with RabbitMQ --- composer.json | 1 + docker-compose.yml | 12 ++++++++++++ 2 files changed, 13 insertions(+) diff --git a/composer.json b/composer.json index b40d24ba..f7da28f9 100644 --- a/composer.json +++ b/composer.json @@ -42,6 +42,7 @@ "nikolaposa/monolog-factory": "^3.1", "ocramius/proxy-manager": "^2.11", "pagerfanta/core": "^3.5", + "php-amqplib/php-amqplib": "^2.0", "php-middleware/request-id": "^4.1", "predis/predis": "^1.1", "pugx/shortid-php": "^1.0", diff --git a/docker-compose.yml b/docker-compose.yml index d9dd776c..3d552f9a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -29,6 +29,7 @@ services: - shlink_redis - shlink_mercure - shlink_mercure_proxy + - shlink_rabbitmq environment: LC_ALL: C extra_hosts: @@ -64,6 +65,7 @@ services: - shlink_redis - shlink_mercure - shlink_mercure_proxy + - shlink_rabbitmq environment: LC_ALL: C extra_hosts: @@ -143,3 +145,13 @@ services: MERCURE_PUBLISHER_JWT_KEY: mercure_jwt_key MERCURE_SUBSCRIBER_JWT_KEY: mercure_jwt_key MERCURE_EXTRA_DIRECTIVES: "cors_origins https://app.shlink.io http://localhost:3000 http://127.0.0.1:3000" + + shlink_rabbitmq: + container_name: shlink_rabbitmq + image: rabbitmq:3.9-management-alpine + ports: + - "15672:15672" + - "5672:5672" + environment: + RABBITMQ_DEFAULT_USER: "rabbit" + RABBITMQ_DEFAULT_PASS: "rabbit" From bd3bb67949d366fde8609f17fc7a2f9b7aa7f18d Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 11 Dec 2021 17:07:40 +0100 Subject: [PATCH 58/66] Added dependencies and config to integrate with Rabbit MQ --- composer.json | 2 +- config/autoload/rabbit.global.php | 49 +++++++++++++++++++++++++++ config/autoload/rabbit.local.php.dist | 13 +++++++ data/infra/php.Dockerfile | 3 ++ data/infra/swoole.Dockerfile | 3 ++ 5 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 config/autoload/rabbit.global.php create mode 100644 config/autoload/rabbit.local.php.dist diff --git a/composer.json b/composer.json index f7da28f9..2cfe4e14 100644 --- a/composer.json +++ b/composer.json @@ -42,7 +42,7 @@ "nikolaposa/monolog-factory": "^3.1", "ocramius/proxy-manager": "^2.11", "pagerfanta/core": "^3.5", - "php-amqplib/php-amqplib": "^2.0", + "php-amqplib/php-amqplib": "^3.1", "php-middleware/request-id": "^4.1", "predis/predis": "^1.1", "pugx/shortid-php": "^1.0", diff --git a/config/autoload/rabbit.global.php b/config/autoload/rabbit.global.php new file mode 100644 index 00000000..113a0048 --- /dev/null +++ b/config/autoload/rabbit.global.php @@ -0,0 +1,49 @@ + [ + 'host' => env('RABBITMQ_HOST'), + 'port' => env('RABBITMQ_PORT', '5672'), + 'user' => env('RABBITMQ_USER'), + 'password' => env('RABBITMQ_PASSWORD'), + 'vhost' => env('RABBITMQ_VHOST', '/'), + 'exchange' => env('RABBITMQ_EXCHANGE', 'shlink-exchange'), + 'queue' => env('RABBITMQ_QUEUE', 'shlink-queue'), + ], + + 'dependencies' => [ + 'factories' => [ + AMQPStreamConnection::class => ConfigAbstractFactory::class, + ], + 'delegators' => [ + AMQPStreamConnection::class => [ + LazyServiceFactory::class, + ], + ], + 'lazy_services' => [ + 'class_map' => [ + AMQPStreamConnection::class => AMQPStreamConnection::class, + ], + ], + ], + + ConfigAbstractFactory::class => [ + AMQPStreamConnection::class => [ + 'config.rabbit.host', + 'config.rabbit.port', + 'config.rabbit.user', + 'config.rabbit.password', + 'config.rabbit.vhost', + ], + ], + +]; diff --git a/config/autoload/rabbit.local.php.dist b/config/autoload/rabbit.local.php.dist new file mode 100644 index 00000000..2425a2c5 --- /dev/null +++ b/config/autoload/rabbit.local.php.dist @@ -0,0 +1,13 @@ + [ + 'host' => 'shlink_rabbitmq', + 'user' => 'rabbit', + 'password' => 'rabbit', + ], + +]; diff --git a/data/infra/php.Dockerfile b/data/infra/php.Dockerfile index 86f95361..96556869 100644 --- a/data/infra/php.Dockerfile +++ b/data/infra/php.Dockerfile @@ -34,6 +34,9 @@ RUN docker-php-ext-install pdo_pgsql RUN apk add --no-cache gmp-dev RUN docker-php-ext-install gmp +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 \ diff --git a/data/infra/swoole.Dockerfile b/data/infra/swoole.Dockerfile index 74b83d07..570ca2a9 100644 --- a/data/infra/swoole.Dockerfile +++ b/data/infra/swoole.Dockerfile @@ -36,6 +36,9 @@ RUN docker-php-ext-install pdo_pgsql RUN apk add --no-cache gmp-dev RUN docker-php-ext-install gmp +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 \ From 966620f840e199acca81e8f12f19a9bac94cbdf0 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 11 Dec 2021 21:04:16 +0100 Subject: [PATCH 59/66] Created event listener to send visits to a RabbitMQ instance --- composer.json | 2 +- config/autoload/rabbit.global.php | 3 +- config/autoload/rabbit.local.php.dist | 1 + docs/async-api/async-api.json | 6 +- .../Core/config/event_dispatcher.config.php | 13 +++ .../EventDispatcher/NotifyVisitToRabbit.php | 103 ++++++++++++++++++ .../src/Mercure/MercureUpdatesGenerator.php | 15 +-- 7 files changed, 126 insertions(+), 17 deletions(-) create mode 100644 module/Core/src/EventDispatcher/NotifyVisitToRabbit.php diff --git a/composer.json b/composer.json index 2cfe4e14..7a0a8542 100644 --- a/composer.json +++ b/composer.json @@ -48,7 +48,7 @@ "pugx/shortid-php": "^1.0", "ramsey/uuid": "^4.2", "rlanvin/php-ip": "dev-master#6b3a785 as 3.0", - "shlinkio/shlink-common": "dev-main#c2e3442 as 4.2", + "shlinkio/shlink-common": "dev-main#e7fdff3 as 4.2", "shlinkio/shlink-config": "^1.4", "shlinkio/shlink-event-dispatcher": "dev-main#3925299 as 2.3", "shlinkio/shlink-importer": "dev-main#d099072 as 2.5", diff --git a/config/autoload/rabbit.global.php b/config/autoload/rabbit.global.php index 113a0048..a17e9887 100644 --- a/config/autoload/rabbit.global.php +++ b/config/autoload/rabbit.global.php @@ -11,13 +11,12 @@ use function Shlinkio\Shlink\Common\env; return [ 'rabbit' => [ + 'enabled' => (bool) env('RABBITMQ_ENABLED', false), 'host' => env('RABBITMQ_HOST'), 'port' => env('RABBITMQ_PORT', '5672'), 'user' => env('RABBITMQ_USER'), 'password' => env('RABBITMQ_PASSWORD'), 'vhost' => env('RABBITMQ_VHOST', '/'), - 'exchange' => env('RABBITMQ_EXCHANGE', 'shlink-exchange'), - 'queue' => env('RABBITMQ_QUEUE', 'shlink-queue'), ], 'dependencies' => [ diff --git a/config/autoload/rabbit.local.php.dist b/config/autoload/rabbit.local.php.dist index 2425a2c5..141b4b8b 100644 --- a/config/autoload/rabbit.local.php.dist +++ b/config/autoload/rabbit.local.php.dist @@ -5,6 +5,7 @@ declare(strict_types=1); return [ 'rabbit' => [ + 'enabled' => true, 'host' => 'shlink_rabbitmq', 'user' => 'rabbit', 'password' => 'rabbit', diff --git a/docs/async-api/async-api.json b/docs/async-api/async-api.json index 0b546377..82da91c5 100644 --- a/docs/async-api/async-api.json +++ b/docs/async-api/async-api.json @@ -11,7 +11,7 @@ }, "defaultContentType": "application/json", "channels": { - "http://shlink.io/new-visit": { + "https://shlink.io/new-visit": { "subscribe": { "summary": "Receive information about any new visit occurring on any short URL.", "operationId": "newVisit", @@ -31,7 +31,7 @@ } } }, - "http://shlink.io/new-visit/{shortCode}": { + "https://shlink.io/new-visit/{shortCode}": { "parameters": { "shortCode": { "description": "The short code of the short URL", @@ -59,7 +59,7 @@ } } }, - "http://shlink.io/new-orphan-visit": { + "https://shlink.io/new-orphan-visit": { "subscribe": { "summary": "Receive information about any new orphan visit.", "operationId": "newOrphanVisit", diff --git a/module/Core/config/event_dispatcher.config.php b/module/Core/config/event_dispatcher.config.php index 5256bc92..a0f09beb 100644 --- a/module/Core/config/event_dispatcher.config.php +++ b/module/Core/config/event_dispatcher.config.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace Shlinkio\Shlink\Core; use Laminas\ServiceManager\AbstractFactory\ConfigAbstractFactory; +use PhpAmqpLib\Connection\AMQPStreamConnection; use Psr\EventDispatcher\EventDispatcherInterface; use Shlinkio\Shlink\CLI\Util\GeolocationDbUpdater; use Shlinkio\Shlink\IpGeolocation\GeoLite2\DbUpdater; @@ -22,6 +23,7 @@ return [ 'async' => [ EventDispatcher\Event\VisitLocated::class => [ EventDispatcher\NotifyVisitToMercure::class, + EventDispatcher\NotifyVisitToRabbit::class, EventDispatcher\NotifyVisitToWebHooks::class, EventDispatcher\UpdateGeoLiteDb::class, ], @@ -33,6 +35,7 @@ return [ EventDispatcher\LocateVisit::class => ConfigAbstractFactory::class, EventDispatcher\NotifyVisitToWebHooks::class => ConfigAbstractFactory::class, EventDispatcher\NotifyVisitToMercure::class => ConfigAbstractFactory::class, + EventDispatcher\NotifyVisitToRabbit::class => ConfigAbstractFactory::class, EventDispatcher\UpdateGeoLiteDb::class => ConfigAbstractFactory::class, ], @@ -40,6 +43,9 @@ return [ EventDispatcher\NotifyVisitToMercure::class => [ EventDispatcher\CloseDbConnectionEventListenerDelegator::class, ], + EventDispatcher\NotifyVisitToRabbit::class => [ + EventDispatcher\CloseDbConnectionEventListenerDelegator::class, + ], EventDispatcher\NotifyVisitToWebHooks::class => [ EventDispatcher\CloseDbConnectionEventListenerDelegator::class, ], @@ -68,6 +74,13 @@ return [ 'em', 'Logger_Shlink', ], + EventDispatcher\NotifyVisitToRabbit::class => [ + AMQPStreamConnection::class, + 'em', + 'Logger_Shlink', + Visit\Transformer\OrphanVisitDataTransformer::class, + 'config.rabbit.enabled', + ], EventDispatcher\UpdateGeoLiteDb::class => [GeolocationDbUpdater::class, 'Logger_Shlink'], ], diff --git a/module/Core/src/EventDispatcher/NotifyVisitToRabbit.php b/module/Core/src/EventDispatcher/NotifyVisitToRabbit.php new file mode 100644 index 00000000..426b02bb --- /dev/null +++ b/module/Core/src/EventDispatcher/NotifyVisitToRabbit.php @@ -0,0 +1,103 @@ +isEnabled) { + return; + } + + $visitId = $shortUrlLocated->visitId(); + + /** @var Visit|null $visit */ + $visit = $this->em->find(Visit::class, $visitId); + if ($visit === null) { + $this->logger->warning('Tried to notify RabbitMQ for visit with id "{visitId}", but it does not exist.', [ + 'visitId' => $visitId, + ]); + return; + } + + if (! $this->connection->isConnected()) { + $this->connection->reconnect(); + } + + $queues = $this->determineQueuesToPublishTo($visit); + $message = $this->visitToMessage($visit); + + try { + $channel = $this->connection->channel(); + + foreach ($queues as $queue) { + // Declare an exchange and a queue that will persist server restarts + $exchange = $queue; // We use the same name for the exchange and the queue + $channel->exchange_declare($exchange, AMQPExchangeType::DIRECT, false, true, false); + $channel->queue_declare($queue, false, true, false, false); + + // Bind the exchange and the queue together, and publish the message + $channel->queue_bind($queue, $exchange); + $channel->basic_publish($message, $exchange); + } + + $channel->close(); + } catch (Throwable $e) { + $this->logger->debug('Error while trying to notify RabbitMQ with new visit. {e}', ['e' => $e]); + } finally { + $this->connection->close(); + } + } + + /** + * @return string[] + */ + private function determineQueuesToPublishTo(Visit $visit): array + { + if ($visit->isOrphan()) { + return [self::NEW_ORPHAN_VISIT_QUEUE]; + } + + return [ + self::NEW_VISIT_QUEUE, + sprintf('%s/%s', self::NEW_VISIT_QUEUE, $visit->getShortUrl()?->getShortCode()), + ]; + } + + private function visitToMessage(Visit $visit): AMQPMessage + { + $messageBody = json_encode(! $visit->isOrphan() ? $visit : $this->orphanVisitTransformer->transform($visit)); + return new AMQPMessage($messageBody, [ + 'content_type' => 'application/json', + 'delivery_mode' => AMQPMessage::DELIVERY_MODE_PERSISTENT, + ]); + } +} diff --git a/module/Core/src/Mercure/MercureUpdatesGenerator.php b/module/Core/src/Mercure/MercureUpdatesGenerator.php index cc0f785a..74b85388 100644 --- a/module/Core/src/Mercure/MercureUpdatesGenerator.php +++ b/module/Core/src/Mercure/MercureUpdatesGenerator.php @@ -8,11 +8,9 @@ use Shlinkio\Shlink\Common\Rest\DataTransformerInterface; use Shlinkio\Shlink\Core\Entity\Visit; use Symfony\Component\Mercure\Update; -use function json_encode; +use function Shlinkio\Shlink\Common\json_encode; use function sprintf; -use const JSON_THROW_ON_ERROR; - final class MercureUpdatesGenerator implements MercureUpdatesGeneratorInterface { private const NEW_VISIT_TOPIC = 'https://shlink.io/new-visit'; @@ -26,7 +24,7 @@ final class MercureUpdatesGenerator implements MercureUpdatesGeneratorInterface public function newVisitUpdate(Visit $visit): Update { - return new Update(self::NEW_VISIT_TOPIC, $this->serialize([ + return new Update(self::NEW_VISIT_TOPIC, json_encode([ 'shortUrl' => $this->shortUrlTransformer->transform($visit->getShortUrl()), 'visit' => $visit, ])); @@ -34,7 +32,7 @@ final class MercureUpdatesGenerator implements MercureUpdatesGeneratorInterface public function newOrphanVisitUpdate(Visit $visit): Update { - return new Update(self::NEW_ORPHAN_VISIT_TOPIC, $this->serialize([ + return new Update(self::NEW_ORPHAN_VISIT_TOPIC, json_encode([ 'visit' => $this->orphanVisitTransformer->transform($visit), ])); } @@ -44,14 +42,9 @@ final class MercureUpdatesGenerator implements MercureUpdatesGeneratorInterface $shortUrl = $visit->getShortUrl(); $topic = sprintf('%s/%s', self::NEW_VISIT_TOPIC, $shortUrl?->getShortCode()); - return new Update($topic, $this->serialize([ + return new Update($topic, json_encode([ 'shortUrl' => $this->shortUrlTransformer->transform($shortUrl), 'visit' => $visit, ])); } - - private function serialize(array $data): string - { - return json_encode($data, JSON_THROW_ON_ERROR); - } } From 0bcefda60d51571b35a7250a3942ac201ebd9967 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 11 Dec 2021 21:44:56 +0100 Subject: [PATCH 60/66] Added sockets and bcmath extensions to docker image --- Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index d26c7848..30ca29e3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,8 +11,8 @@ WORKDIR /etc/shlink # Install required PHP extensions RUN \ - # Install mysql and calendar - docker-php-ext-install -j"$(nproc)" pdo_mysql calendar && \ + # Install extensions with no extra dependencies + docker-php-ext-install -j"$(nproc)" pdo_mysql calendar sockets bcmath && \ # Install sqlite apk add --no-cache sqlite-libs sqlite-dev && \ docker-php-ext-install -j"$(nproc)" pdo_sqlite && \ From cb1705b6e801bf3b193970f1ebce8d9c87fcadf7 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 11 Dec 2021 22:18:46 +0100 Subject: [PATCH 61/66] Created NotifyVisitToRabbitTest --- CHANGELOG.md | 1 - composer.json | 2 +- .../EventDispatcher/NotifyVisitToRabbit.php | 3 +- .../NotifyVisitToRabbitTest.php | 178 ++++++++++++++++++ 4 files changed, 180 insertions(+), 4 deletions(-) create mode 100644 module/Core/test/EventDispatcher/NotifyVisitToRabbitTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index d0cbf02b..2f6d10c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,7 +29,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this * [#1218](https://github.com/shlinkio/shlink/issues/1218) Updated to symfony/mercure 0.6. * [#1223](https://github.com/shlinkio/shlink/issues/1223) Updated to phpstan 1.0. * Added `domain` field to `DeleteShortUrlException` exception. -* [#1001](https://github.com/shlinkio/shlink/issues/1001) Increased required MSI to 83%. ### Deprecated * [#1260](https://github.com/shlinkio/shlink/issues/1260) Deprecated `USE_HTTPS` env var that was added in previous release, in favor of the new `IS_HTTPS_ENABLED`. diff --git a/composer.json b/composer.json index 7a0a8542..e244fbfc 100644 --- a/composer.json +++ b/composer.json @@ -142,7 +142,7 @@ "test:api": "bin/test/run-api-tests.sh", "test:api:ci": "GENERATE_COVERAGE=yes composer test:api", "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=83", + "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", "infect:ci:api": "@infect:ci:base --coverage=build/coverage-api --min-msi=80 --configuration=infection-api.json", "infect:ci": "@parallel infect:ci:unit infect:ci:db infect:ci:api", diff --git a/module/Core/src/EventDispatcher/NotifyVisitToRabbit.php b/module/Core/src/EventDispatcher/NotifyVisitToRabbit.php index 426b02bb..6ff79eb8 100644 --- a/module/Core/src/EventDispatcher/NotifyVisitToRabbit.php +++ b/module/Core/src/EventDispatcher/NotifyVisitToRabbit.php @@ -38,9 +38,8 @@ class NotifyVisitToRabbit } $visitId = $shortUrlLocated->visitId(); - - /** @var Visit|null $visit */ $visit = $this->em->find(Visit::class, $visitId); + if ($visit === null) { $this->logger->warning('Tried to notify RabbitMQ for visit with id "{visitId}", but it does not exist.', [ 'visitId' => $visitId, diff --git a/module/Core/test/EventDispatcher/NotifyVisitToRabbitTest.php b/module/Core/test/EventDispatcher/NotifyVisitToRabbitTest.php new file mode 100644 index 00000000..73a970fb --- /dev/null +++ b/module/Core/test/EventDispatcher/NotifyVisitToRabbitTest.php @@ -0,0 +1,178 @@ +channel = $this->prophesize(AMQPChannel::class); + + $this->connection = $this->prophesize(AMQPStreamConnection::class); + $this->connection->isConnected()->willReturn(false); + $this->connection->channel()->willReturn($this->channel->reveal()); + + $this->em = $this->prophesize(EntityManagerInterface::class); + $this->logger = $this->prophesize(LoggerInterface::class); + + $this->listener = new NotifyVisitToRabbit( + $this->connection->reveal(), + $this->em->reveal(), + $this->logger->reveal(), + new OrphanVisitDataTransformer(), + true, + ); + } + + /** @test */ + public function doesNothingWhenTheFeatureIsNotEnabled(): void + { + $listener = new NotifyVisitToRabbit( + $this->connection->reveal(), + $this->em->reveal(), + $this->logger->reveal(), + new OrphanVisitDataTransformer(), + false, + ); + + $listener(new VisitLocated('123')); + + $this->em->find(Argument::cetera())->shouldNotHaveBeenCalled(); + $this->logger->warning(Argument::cetera())->shouldNotHaveBeenCalled(); + $this->logger->debug(Argument::cetera())->shouldNotHaveBeenCalled(); + $this->connection->isConnected()->shouldNotHaveBeenCalled(); + $this->connection->close()->shouldNotHaveBeenCalled(); + } + + /** @test */ + public function notificationsAreNotSentWhenVisitCannotBeFound(): void + { + $visitId = '123'; + $findVisit = $this->em->find(Visit::class, $visitId)->willReturn(null); + $logWarning = $this->logger->warning( + 'Tried to notify RabbitMQ for visit with id "{visitId}", but it does not exist.', + ['visitId' => $visitId], + ); + + ($this->listener)(new VisitLocated($visitId)); + + $findVisit->shouldHaveBeenCalledOnce(); + $logWarning->shouldHaveBeenCalledOnce(); + $this->logger->debug(Argument::cetera())->shouldNotHaveBeenCalled(); + $this->connection->isConnected()->shouldNotHaveBeenCalled(); + $this->connection->close()->shouldNotHaveBeenCalled(); + } + + /** + * @test + * @dataProvider provideVisits + */ + public function expectedChannelsAreNotifiedBasedOnTheVisitType(Visit $visit, array $expectedChannels): void + { + $visitId = '123'; + $findVisit = $this->em->find(Visit::class, $visitId)->willReturn($visit); + $argumentWithExpectedChannel = Argument::that(fn (string $channel) => contains($expectedChannels, $channel)); + + ($this->listener)(new VisitLocated($visitId)); + + $findVisit->shouldHaveBeenCalledOnce(); + $this->channel->exchange_declare($argumentWithExpectedChannel, Argument::cetera())->shouldHaveBeenCalledTimes( + count($expectedChannels), + ); + $this->channel->queue_declare($argumentWithExpectedChannel, Argument::cetera())->shouldHaveBeenCalledTimes( + count($expectedChannels), + ); + $this->channel->queue_bind( + $argumentWithExpectedChannel, + $argumentWithExpectedChannel, + )->shouldHaveBeenCalledTimes(count($expectedChannels)); + $this->channel->basic_publish(Argument::any(), $argumentWithExpectedChannel)->shouldHaveBeenCalledTimes( + count($expectedChannels), + ); + $this->channel->close()->shouldHaveBeenCalledOnce(); + $this->connection->reconnect()->shouldHaveBeenCalledOnce(); + $this->connection->close()->shouldHaveBeenCalledOnce(); + $this->logger->debug(Argument::cetera())->shouldNotHaveBeenCalled(); + } + + public function provideVisits(): iterable + { + $visitor = Visitor::emptyInstance(); + + yield 'orphan visit' => [Visit::forBasePath($visitor), ['https://shlink.io/new-orphan-visit']]; + yield 'non-orphan visit' => [ + Visit::forValidShortUrl( + ShortUrl::fromMeta(ShortUrlMeta::fromRawData([ + 'longUrl' => 'foo', + 'customSlug' => 'bar', + ])), + $visitor, + ), + ['https://shlink.io/new-visit', 'https://shlink.io/new-visit/bar'], + ]; + } + + /** + * @test + * @dataProvider provideExceptions + */ + public function printsDebugMessageInCaseOfError(Throwable $e): void + { + $visitId = '123'; + $findVisit = $this->em->find(Visit::class, $visitId)->willReturn(Visit::forBasePath(Visitor::emptyInstance())); + $channel = $this->connection->channel()->willThrow($e); + + ($this->listener)(new VisitLocated($visitId)); + + $this->logger->debug( + 'Error while trying to notify RabbitMQ with new visit. {e}', + ['e' => $e], + )->shouldHaveBeenCalledOnce(); + $this->connection->close()->shouldHaveBeenCalledOnce(); + $this->connection->reconnect()->shouldHaveBeenCalledOnce(); + $findVisit->shouldHaveBeenCalledOnce(); + $channel->shouldHaveBeenCalledOnce(); + $this->channel->close()->shouldNotHaveBeenCalled(); + } + + public function provideExceptions(): iterable + { + yield [new RuntimeException('RuntimeException Error')]; + yield [new Exception('Exception Error')]; + yield [new DomainException('DomainException Error')]; + } +} From 8e5730f37409312f7bbbc7424514e7f74b2c16d9 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 12 Dec 2021 10:32:57 +0100 Subject: [PATCH 62/66] Renamed Rabbit instances to use RabbitMq --- README.md | 3 ++- config/autoload/rabbit.global.php | 14 +++++++------- config/autoload/rabbit.local.php.dist | 2 +- module/Core/config/event_dispatcher.config.php | 10 +++++----- ...VisitToRabbit.php => NotifyVisitToRabbitMq.php} | 2 +- ...abbitTest.php => NotifyVisitToRabbitMqTest.php} | 10 +++++----- 6 files changed, 21 insertions(+), 20 deletions(-) rename module/Core/src/EventDispatcher/{NotifyVisitToRabbit.php => NotifyVisitToRabbitMq.php} (99%) rename module/Core/test/EventDispatcher/{NotifyVisitToRabbitTest.php => NotifyVisitToRabbitMqTest.php} (96%) diff --git a/README.md b/README.md index 4fe5f5cf..9aad62d9 100644 --- a/README.md +++ b/README.md @@ -34,10 +34,11 @@ The idea is that you can just generate a container using the image and provide t First, make sure the host where you are going to run shlink fulfills these requirements: -* PHP 8.0 +* PHP 8.0 or 8.1 * The next PHP extensions: json, curl, pdo, intl, gd and gmp. * apcu extension is recommended if you don't plan to use swoole or openswoole. * xml extension is required if you want to generate QR codes in svg format. + * sockets and bcmath extensions are required if you want to integrate with a RabbitMQ instance. * MySQL, MariaDB, PostgreSQL, Microsoft SQL Server or SQLite. * The web server of your choice with PHP integration (Apache or Nginx recommended). diff --git a/config/autoload/rabbit.global.php b/config/autoload/rabbit.global.php index a17e9887..b08dccf2 100644 --- a/config/autoload/rabbit.global.php +++ b/config/autoload/rabbit.global.php @@ -10,10 +10,10 @@ use function Shlinkio\Shlink\Common\env; return [ - 'rabbit' => [ + 'rabbitmq' => [ 'enabled' => (bool) env('RABBITMQ_ENABLED', false), 'host' => env('RABBITMQ_HOST'), - 'port' => env('RABBITMQ_PORT', '5672'), + 'port' => (int) env('RABBITMQ_PORT', '5672'), 'user' => env('RABBITMQ_USER'), 'password' => env('RABBITMQ_PASSWORD'), 'vhost' => env('RABBITMQ_VHOST', '/'), @@ -37,11 +37,11 @@ return [ ConfigAbstractFactory::class => [ AMQPStreamConnection::class => [ - 'config.rabbit.host', - 'config.rabbit.port', - 'config.rabbit.user', - 'config.rabbit.password', - 'config.rabbit.vhost', + 'config.rabbitmq.host', + 'config.rabbitmq.port', + 'config.rabbitmq.user', + 'config.rabbitmq.password', + 'config.rabbitmq.vhost', ], ], diff --git a/config/autoload/rabbit.local.php.dist b/config/autoload/rabbit.local.php.dist index 141b4b8b..83cd4a88 100644 --- a/config/autoload/rabbit.local.php.dist +++ b/config/autoload/rabbit.local.php.dist @@ -4,7 +4,7 @@ declare(strict_types=1); return [ - 'rabbit' => [ + 'rabbitmq' => [ 'enabled' => true, 'host' => 'shlink_rabbitmq', 'user' => 'rabbit', diff --git a/module/Core/config/event_dispatcher.config.php b/module/Core/config/event_dispatcher.config.php index a0f09beb..d47cc128 100644 --- a/module/Core/config/event_dispatcher.config.php +++ b/module/Core/config/event_dispatcher.config.php @@ -23,7 +23,7 @@ return [ 'async' => [ EventDispatcher\Event\VisitLocated::class => [ EventDispatcher\NotifyVisitToMercure::class, - EventDispatcher\NotifyVisitToRabbit::class, + EventDispatcher\NotifyVisitToRabbitMq::class, EventDispatcher\NotifyVisitToWebHooks::class, EventDispatcher\UpdateGeoLiteDb::class, ], @@ -35,7 +35,7 @@ return [ EventDispatcher\LocateVisit::class => ConfigAbstractFactory::class, EventDispatcher\NotifyVisitToWebHooks::class => ConfigAbstractFactory::class, EventDispatcher\NotifyVisitToMercure::class => ConfigAbstractFactory::class, - EventDispatcher\NotifyVisitToRabbit::class => ConfigAbstractFactory::class, + EventDispatcher\NotifyVisitToRabbitMq::class => ConfigAbstractFactory::class, EventDispatcher\UpdateGeoLiteDb::class => ConfigAbstractFactory::class, ], @@ -43,7 +43,7 @@ return [ EventDispatcher\NotifyVisitToMercure::class => [ EventDispatcher\CloseDbConnectionEventListenerDelegator::class, ], - EventDispatcher\NotifyVisitToRabbit::class => [ + EventDispatcher\NotifyVisitToRabbitMq::class => [ EventDispatcher\CloseDbConnectionEventListenerDelegator::class, ], EventDispatcher\NotifyVisitToWebHooks::class => [ @@ -74,12 +74,12 @@ return [ 'em', 'Logger_Shlink', ], - EventDispatcher\NotifyVisitToRabbit::class => [ + EventDispatcher\NotifyVisitToRabbitMq::class => [ AMQPStreamConnection::class, 'em', 'Logger_Shlink', Visit\Transformer\OrphanVisitDataTransformer::class, - 'config.rabbit.enabled', + 'config.rabbitmq.enabled', ], EventDispatcher\UpdateGeoLiteDb::class => [GeolocationDbUpdater::class, 'Logger_Shlink'], ], diff --git a/module/Core/src/EventDispatcher/NotifyVisitToRabbit.php b/module/Core/src/EventDispatcher/NotifyVisitToRabbitMq.php similarity index 99% rename from module/Core/src/EventDispatcher/NotifyVisitToRabbit.php rename to module/Core/src/EventDispatcher/NotifyVisitToRabbitMq.php index 6ff79eb8..f05ecf64 100644 --- a/module/Core/src/EventDispatcher/NotifyVisitToRabbit.php +++ b/module/Core/src/EventDispatcher/NotifyVisitToRabbitMq.php @@ -17,7 +17,7 @@ use Throwable; use function Shlinkio\Shlink\Common\json_encode; use function sprintf; -class NotifyVisitToRabbit +class NotifyVisitToRabbitMq { private const NEW_VISIT_QUEUE = 'https://shlink.io/new-visit'; private const NEW_ORPHAN_VISIT_QUEUE = 'https://shlink.io/new-orphan-visit'; diff --git a/module/Core/test/EventDispatcher/NotifyVisitToRabbitTest.php b/module/Core/test/EventDispatcher/NotifyVisitToRabbitMqTest.php similarity index 96% rename from module/Core/test/EventDispatcher/NotifyVisitToRabbitTest.php rename to module/Core/test/EventDispatcher/NotifyVisitToRabbitMqTest.php index 73a970fb..778da889 100644 --- a/module/Core/test/EventDispatcher/NotifyVisitToRabbitTest.php +++ b/module/Core/test/EventDispatcher/NotifyVisitToRabbitMqTest.php @@ -18,7 +18,7 @@ use RuntimeException; use Shlinkio\Shlink\Core\Entity\ShortUrl; use Shlinkio\Shlink\Core\Entity\Visit; use Shlinkio\Shlink\Core\EventDispatcher\Event\VisitLocated; -use Shlinkio\Shlink\Core\EventDispatcher\NotifyVisitToRabbit; +use Shlinkio\Shlink\Core\EventDispatcher\NotifyVisitToRabbitMq; use Shlinkio\Shlink\Core\Model\ShortUrlMeta; use Shlinkio\Shlink\Core\Model\Visitor; use Shlinkio\Shlink\Core\Visit\Transformer\OrphanVisitDataTransformer; @@ -27,11 +27,11 @@ use Throwable; use function count; use function Functional\contains; -class NotifyVisitToRabbitTest extends TestCase +class NotifyVisitToRabbitMqTest extends TestCase { use ProphecyTrait; - private NotifyVisitToRabbit $listener; + private NotifyVisitToRabbitMq $listener; private ObjectProphecy $connection; private ObjectProphecy $em; private ObjectProphecy $logger; @@ -49,7 +49,7 @@ class NotifyVisitToRabbitTest extends TestCase $this->em = $this->prophesize(EntityManagerInterface::class); $this->logger = $this->prophesize(LoggerInterface::class); - $this->listener = new NotifyVisitToRabbit( + $this->listener = new NotifyVisitToRabbitMq( $this->connection->reveal(), $this->em->reveal(), $this->logger->reveal(), @@ -61,7 +61,7 @@ class NotifyVisitToRabbitTest extends TestCase /** @test */ public function doesNothingWhenTheFeatureIsNotEnabled(): void { - $listener = new NotifyVisitToRabbit( + $listener = new NotifyVisitToRabbitMq( $this->connection->reveal(), $this->em->reveal(), $this->logger->reveal(), From 54dcaaac0ceaf39e1510d7c46b9e7073174e70be Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 12 Dec 2021 11:24:58 +0100 Subject: [PATCH 63/66] Updated to an installer version with support for RabbitMQ --- CHANGELOG.md | 6 ++++++ composer.json | 2 +- config/autoload/installer.global.php | 6 ++++++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f6d10c1..18802cc2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this * The `GET /domains` endpoint includes a new `defaultRedirects` property in the response, with the default redirects set via config or env vars. * The `INVALID_SHORT_URL_REDIRECT_TO`, `REGULAR_404_REDIRECT_TO` and `BASE_URL_REDIRECT_TO` env vars are now deprecated, and should be replaced by `DEFAULT_INVALID_SHORT_URL_REDIRECT`, `DEFAULT_REGULAR_404_REDIRECT` and `DEFAULT_BASE_URL_REDIRECT` respectively. Deprecated ones will continue to work until v3.0.0, where they will be removed. +* [#868](https://github.com/shlinkio/shlink/issues/868) Added support to publish real-time updates in a RabbitMQ server. + + Shlink will create new exchanges and queues for every topic documented in the [Async API spec](https://api-spec.shlink.io/async-api/), meaning, you will have one queue for orphan visits, one for regular visits, and one queue for every short URL with its visits. + + The RabbitMQ server config can be provided via installer config options, or via environment variables. + * [#1204](https://github.com/shlinkio/shlink/issues/1204) Added support for `openswoole` and migrated official docker image to `openswoole`. * [#1242](https://github.com/shlinkio/shlink/issues/1242) Added support to import urls and visits from YOURLS. diff --git a/composer.json b/composer.json index e244fbfc..7064a2b3 100644 --- a/composer.json +++ b/composer.json @@ -52,7 +52,7 @@ "shlinkio/shlink-config": "^1.4", "shlinkio/shlink-event-dispatcher": "dev-main#3925299 as 2.3", "shlinkio/shlink-importer": "dev-main#d099072 as 2.5", - "shlinkio/shlink-installer": "dev-develop#7dd00fb as 6.3", + "shlinkio/shlink-installer": "^6.3", "shlinkio/shlink-ip-geolocation": "^2.2", "symfony/console": "^6.0 || ^5.4", "symfony/filesystem": "^6.0 || ^5.4", diff --git a/config/autoload/installer.global.php b/config/autoload/installer.global.php index def478af..238dea42 100644 --- a/config/autoload/installer.global.php +++ b/config/autoload/installer.global.php @@ -57,6 +57,12 @@ return [ Option\QrCode\DefaultFormatConfigOption::class, Option\QrCode\DefaultErrorCorrectionConfigOption::class, Option\QrCode\DefaultRoundBlockSizeConfigOption::class, + Option\RabbitMq\RabbitMqEnabledConfigOption::class, + Option\RabbitMq\RabbitMqHostConfigOption::class, + Option\RabbitMq\RabbitMqPortConfigOption::class, + Option\RabbitMq\RabbitMqUserConfigOption::class, + Option\RabbitMq\RabbitMqPasswordConfigOption::class, + Option\RabbitMq\RabbitMqVhostConfigOption::class, ], 'installation_commands' => [ From 30a7c55e844a40468b93acc78de3b73ddffd1e44 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 12 Dec 2021 13:30:18 +0100 Subject: [PATCH 64/66] Migrated to a new lib to match IP addresses with ranges --- composer.json | 4 +-- module/Core/src/Visit/RequestTracker.php | 37 ++++++++++++------------ 2 files changed, 20 insertions(+), 21 deletions(-) diff --git a/composer.json b/composer.json index 7064a2b3..8a6651c8 100644 --- a/composer.json +++ b/composer.json @@ -38,6 +38,7 @@ "mezzio/mezzio-fastroute": "^3.3", "mezzio/mezzio-problem-details": "^1.5", "mezzio/mezzio-swoole": "^3.5", + "mlocati/ip-lib": "^1.17", "monolog/monolog": "^2.3", "nikolaposa/monolog-factory": "^3.1", "ocramius/proxy-manager": "^2.11", @@ -47,14 +48,13 @@ "predis/predis": "^1.1", "pugx/shortid-php": "^1.0", "ramsey/uuid": "^4.2", - "rlanvin/php-ip": "dev-master#6b3a785 as 3.0", "shlinkio/shlink-common": "dev-main#e7fdff3 as 4.2", "shlinkio/shlink-config": "^1.4", "shlinkio/shlink-event-dispatcher": "dev-main#3925299 as 2.3", "shlinkio/shlink-importer": "dev-main#d099072 as 2.5", "shlinkio/shlink-installer": "^6.3", "shlinkio/shlink-ip-geolocation": "^2.2", - "symfony/console": "^6.0 || ^5.4", + "symfony/console": "^5.4", "symfony/filesystem": "^6.0 || ^5.4", "symfony/lock": "^6.0 || ^5.4", "symfony/mercure": "^0.6", diff --git a/module/Core/src/Visit/RequestTracker.php b/module/Core/src/Visit/RequestTracker.php index eee75ea4..7cefa8a2 100644 --- a/module/Core/src/Visit/RequestTracker.php +++ b/module/Core/src/Visit/RequestTracker.php @@ -5,9 +5,10 @@ declare(strict_types=1); namespace Shlinkio\Shlink\Core\Visit; use Fig\Http\Message\RequestMethodInterface; -use InvalidArgumentException; +use IPLib\Address\IPv4; +use IPLib\Factory; +use IPLib\Range\RangeInterface; use Mezzio\Router\Middleware\ImplicitHeadMiddleware; -use PhpIP\IP; use Psr\Http\Message\ServerRequestInterface; use Shlinkio\Shlink\Common\Middleware\IpAddressMiddlewareFactory; use Shlinkio\Shlink\Core\Entity\ShortUrl; @@ -73,9 +74,8 @@ class RequestTracker implements RequestTrackerInterface, RequestMethodInterface return false; } - try { - $ip = IP::create($remoteAddr); - } catch (InvalidArgumentException) { + $ip = IPv4::parseString($remoteAddr); + if ($ip === null) { return false; } @@ -83,24 +83,23 @@ class RequestTracker implements RequestTrackerInterface, RequestMethodInterface $disableTrackingFrom = $this->trackingOptions->disableTrackingFrom(); return some($disableTrackingFrom, function (string $value) use ($ip, $remoteAddrParts): bool { - try { - return match (true) { - str_contains($value, '*') => $ip->matches($this->parseValueWithWildcards($value, $remoteAddrParts)), - str_contains($value, '/') => $ip->isIn($value), - default => $ip->matches($value), - }; - } catch (InvalidArgumentException) { - return false; - } + $range = match (true) { + str_contains($value, '*') => $this->parseValueWithWildcards($value, $remoteAddrParts), + default => Factory::parseRangeString($value), + }; + + return $range !== null && $ip->matches($range); }); } - private function parseValueWithWildcards(string $value, array $remoteAddrParts): string + private function parseValueWithWildcards(string $value, array $remoteAddrParts): ?RangeInterface { // Replace wildcard parts with the corresponding ones from the remote address - return implode('.', map( - explode('.', $value), - fn (string $part, int $index) => $part === '*' ? $remoteAddrParts[$index] : $part, - )); + return Factory::parseRangeString( + implode('.', map( + explode('.', $value), + fn (string $part, int $index) => $part === '*' ? $remoteAddrParts[$index] : $part, + )), + ); } } From 959efd17c8f9abf1b62bcbe08a797864cf23be61 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 12 Dec 2021 13:31:08 +0100 Subject: [PATCH 65/66] Updated changelog --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 18802cc2..ee4a64c6 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] +## [2.10.0] - 2021-12-12 ### Added * [#1163](https://github.com/shlinkio/shlink/issues/1163) Allowed setting not-found redirects for default domain in the same way it's done for any other domain. @@ -34,6 +34,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this * [#844](https://github.com/shlinkio/shlink/issues/844) Added mutation checks to API tests. * [#1218](https://github.com/shlinkio/shlink/issues/1218) Updated to symfony/mercure 0.6. * [#1223](https://github.com/shlinkio/shlink/issues/1223) Updated to phpstan 1.0. +* [#1258](https://github.com/shlinkio/shlink/issues/1258) Updated to Symfony 6 components, except symfony/console. * Added `domain` field to `DeleteShortUrlException` exception. ### Deprecated From d082d208e19f1bd674a16272777aa22bf36f3c10 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 12 Dec 2021 17:08:26 +0100 Subject: [PATCH 66/66] Tagged specific versions for shlink packages --- composer.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index 8a6651c8..9652af32 100644 --- a/composer.json +++ b/composer.json @@ -48,10 +48,10 @@ "predis/predis": "^1.1", "pugx/shortid-php": "^1.0", "ramsey/uuid": "^4.2", - "shlinkio/shlink-common": "dev-main#e7fdff3 as 4.2", + "shlinkio/shlink-common": "^4.2", "shlinkio/shlink-config": "^1.4", - "shlinkio/shlink-event-dispatcher": "dev-main#3925299 as 2.3", - "shlinkio/shlink-importer": "dev-main#d099072 as 2.5", + "shlinkio/shlink-event-dispatcher": "^2.3", + "shlinkio/shlink-importer": "^2.5", "shlinkio/shlink-installer": "^6.3", "shlinkio/shlink-ip-geolocation": "^2.2", "symfony/console": "^5.4",