diff --git a/.gitignore b/.gitignore index ce581c01..9c2b2223 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ composer.phar vendor/ .env data/database.sqlite +data/shlink-tests.db data/GeoLite2-City.mmdb docs/swagger-ui* docker-compose.override.yml diff --git a/config/autoload/dependencies.local.php.dist b/config/autoload/dependencies.local.php.dist index 5ce9874b..e7fb274f 100644 --- a/config/autoload/dependencies.local.php.dist +++ b/config/autoload/dependencies.local.php.dist @@ -1,12 +1,23 @@ [ 'lazy_services' => [ 'write_proxy_files' => false, ], + + 'initializers' => [ + function (ContainerInterface $container, $instance) { + if ($instance instanceof Log\LoggerAwareInterface) { + $instance->setLogger($container->get(Log\LoggerInterface::class)); + } + }, + ], ], ]; diff --git a/config/test/bootstrap_api_tests.php b/config/test/bootstrap_api_tests.php index a016f93f..41227dba 100644 --- a/config/test/bootstrap_api_tests.php +++ b/config/test/bootstrap_api_tests.php @@ -19,7 +19,7 @@ $testHelper = $container->get(TestHelper::class); $config = $container->get('config'); $em = $container->get(EntityManager::class); -$testHelper->createTestDb(); +$testHelper->createTestDb($config['entity_manager']['connection']['path']); ApiTest\ApiTestCase::setApiClient($container->get('shlink_test_api_client')); ApiTest\ApiTestCase::setSeedFixturesCallback(function () use ($testHelper, $em, $config) { $testHelper->seedFixtures($em, $config['data_fixtures'] ?? []); diff --git a/config/test/bootstrap_db_tests.php b/config/test/bootstrap_db_tests.php index 58bc2174..ee44849f 100644 --- a/config/test/bootstrap_db_tests.php +++ b/config/test/bootstrap_db_tests.php @@ -14,6 +14,7 @@ if (! file_exists('.env')) { /** @var ContainerInterface $container */ $container = require __DIR__ . '/../container.php'; +$config = $container->get('config'); -$container->get(TestHelper::class)->createTestDb(); +$container->get(TestHelper::class)->createTestDb($config['entity_manager']['connection']['path']); DbTest\DatabaseTestCase::setEntityManager($container->get('em')); diff --git a/config/test/test_config.global.php b/config/test/test_config.global.php index dd794fe4..a0f7bcb8 100644 --- a/config/test/test_config.global.php +++ b/config/test/test_config.global.php @@ -6,7 +6,6 @@ namespace ShlinkioTest\Shlink; use GuzzleHttp\Client; use Zend\ConfigAggregator\ConfigAggregator; use Zend\ServiceManager\Factory\InvokableFactory; -use function realpath; use function sprintf; use function sys_get_temp_dir; @@ -51,7 +50,8 @@ return [ 'entity_manager' => [ 'connection' => [ 'driver' => 'pdo_sqlite', - 'path' => realpath(sys_get_temp_dir()) . '/shlink-tests.db', + 'path' => sys_get_temp_dir() . '/shlink-tests.db', +// 'path' => __DIR__ . '/../../data/shlink-tests.db', ], ], diff --git a/docs/swagger/paths/v1_short-urls.json b/docs/swagger/paths/v1_short-urls.json index b58cd7f1..1bb59a9b 100644 --- a/docs/swagger/paths/v1_short-urls.json +++ b/docs/swagger/paths/v1_short-urls.json @@ -197,6 +197,10 @@ "maxVisits": { "description": "The maximum number of allowed visits for this short code", "type": "number" + }, + "findIfExists": { + "description": "Will force existing matching URL to be returned if found, instead of creating a new one", + "type": "boolean" } } } diff --git a/module/Common/test-db/TestHelper.php b/module/Common/test-db/TestHelper.php index fba79a58..89c8adca 100644 --- a/module/Common/test-db/TestHelper.php +++ b/module/Common/test-db/TestHelper.php @@ -9,15 +9,12 @@ use Doctrine\Common\DataFixtures\Purger\ORMPurger; use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\Process\Process; use function file_exists; -use function realpath; -use function sys_get_temp_dir; use function unlink; class TestHelper { - public function createTestDb(): void + public function createTestDb(string $shlinkDbPath): void { - $shlinkDbPath = realpath(sys_get_temp_dir()) . '/shlink-tests.db'; if (file_exists($shlinkDbPath)) { unlink($shlinkDbPath); } diff --git a/module/Rest/src/Action/ShortUrl/CreateShortUrlAction.php b/module/Rest/src/Action/ShortUrl/CreateShortUrlAction.php index b49fa522..ff79818b 100644 --- a/module/Rest/src/Action/ShortUrl/CreateShortUrlAction.php +++ b/module/Rest/src/Action/ShortUrl/CreateShortUrlAction.php @@ -35,7 +35,8 @@ class CreateShortUrlAction extends AbstractCreateShortUrlAction $this->getOptionalDate($postData, 'validSince'), $this->getOptionalDate($postData, 'validUntil'), $postData['customSlug'] ?? null, - isset($postData['maxVisits']) ? (int) $postData['maxVisits'] : null + $postData['maxVisits'] ?? null, + $postData['findIfExists'] ?? null ) ); } diff --git a/module/Rest/test-api/Action/CreateShortUrlActionTest.php b/module/Rest/test-api/Action/CreateShortUrlActionTest.php index 2307d657..aefb8d6f 100644 --- a/module/Rest/test-api/Action/CreateShortUrlActionTest.php +++ b/module/Rest/test-api/Action/CreateShortUrlActionTest.php @@ -12,7 +12,7 @@ class CreateShortUrlActionTest extends ApiTestCase /** * @test */ - public function createsNewShortUrlWhenOnlyLongUrlIsProvided() + public function createsNewShortUrlWhenOnlyLongUrlIsProvided(): void { $expectedKeys = ['shortCode', 'shortUrl', 'longUrl', 'dateCreated', 'visitsCount', 'tags']; [$statusCode, $payload] = $this->createShortUrl(); @@ -26,7 +26,7 @@ class CreateShortUrlActionTest extends ApiTestCase /** * @test */ - public function createsNewShortUrlWithCustomSlug() + public function createsNewShortUrlWithCustomSlug(): void { [$statusCode, $payload] = $this->createShortUrl(['customSlug' => 'my cool slug']); @@ -37,7 +37,7 @@ class CreateShortUrlActionTest extends ApiTestCase /** * @test */ - public function createsNewShortUrlWithTags() + public function createsNewShortUrlWithTags(): void { [$statusCode, $payload] = $this->createShortUrl(['tags' => ['foo', 'bar', 'baz']]); @@ -49,7 +49,7 @@ class CreateShortUrlActionTest extends ApiTestCase * @test * @dataProvider provideMaxVisits */ - public function createsNewShortUrlWithVisitsLimit(int $maxVisits) + public function createsNewShortUrlWithVisitsLimit(int $maxVisits): void { [$statusCode, ['shortCode' => $shortCode]] = $this->createShortUrl(['maxVisits' => $maxVisits]); @@ -75,7 +75,7 @@ class CreateShortUrlActionTest extends ApiTestCase /** * @test */ - public function createsShortUrlWithValidSince() + public function createsShortUrlWithValidSince(): void { [$statusCode, ['shortCode' => $shortCode]] = $this->createShortUrl([ 'validSince' => Chronos::now()->addDay()->toAtomString(), @@ -91,7 +91,7 @@ class CreateShortUrlActionTest extends ApiTestCase /** * @test */ - public function createsShortUrlWithValidUntil() + public function createsShortUrlWithValidUntil(): void { [$statusCode, ['shortCode' => $shortCode]] = $this->createShortUrl([ 'validUntil' => Chronos::now()->subDay()->toAtomString(), @@ -104,6 +104,76 @@ class CreateShortUrlActionTest extends ApiTestCase $this->assertEquals(self::STATUS_NOT_FOUND, $lastResp->getStatusCode()); } + /** + * @test + * @dataProvider provideMatchingBodies + */ + public function returnsAnExistingShortUrlWhenRequested(array $body): void + { + + [$firstStatusCode, ['shortCode' => $firstShortCode]] = $this->createShortUrl($body); + + $body['findIfExists'] = true; + [$secondStatusCode, ['shortCode' => $secondShortCode]] = $this->createShortUrl($body); + + $this->assertEquals(self::STATUS_OK, $firstStatusCode); + $this->assertEquals(self::STATUS_OK, $secondStatusCode); + $this->assertEquals($firstShortCode, $secondShortCode); + } + + public function provideMatchingBodies(): array + { + $longUrl = 'https://www.alejandrocelaya.com'; + + return [ + 'only long URL' => [['longUrl' => $longUrl]], + 'long URL and tags' => [['longUrl' => $longUrl, 'tags' => ['boo', 'far']]], + 'long URL custom slug' => [['longUrl' => $longUrl, 'customSlug' => 'my cool slug']], + 'several params' => [[ + 'longUrl' => $longUrl, + 'tags' => ['boo', 'far'], + 'validSince' => Chronos::now()->toAtomString(), + 'maxVisits' => 7, + ]], + ]; + } + + /** + * @test + */ + public function returnsErrorWhenRequestingReturnExistingButCustomSlugIsInUse(): void + { + $longUrl = 'https://www.alejandrocelaya.com'; + + [$firstStatusCode] = $this->createShortUrl(['longUrl' => $longUrl]); + [$secondStatusCode] = $this->createShortUrl([ + 'longUrl' => $longUrl, + 'customSlug' => 'custom', + 'findIfExists' => true, + ]); + + $this->assertEquals(self::STATUS_OK, $firstStatusCode); + $this->assertEquals(self::STATUS_BAD_REQUEST, $secondStatusCode); + } + + /** + * @test + */ + public function createsNewShortUrlIfRequestedToFindButThereIsNoMatch(): void + { + [$firstStatusCode, ['shortCode' => $firstShortCode]] = $this->createShortUrl([ + 'longUrl' => 'https://www.alejandrocelaya.com', + ]); + [$secondStatusCode, ['shortCode' => $secondShortCode]] = $this->createShortUrl([ + 'longUrl' => 'https://www.alejandrocelaya.com/projects', + 'findIfExists' => true, + ]); + + $this->assertEquals(self::STATUS_OK, $firstStatusCode); + $this->assertEquals(self::STATUS_OK, $secondStatusCode); + $this->assertNotEquals($firstShortCode, $secondShortCode); + } + /** * @return array { * @var int $statusCode @@ -112,7 +182,9 @@ class CreateShortUrlActionTest extends ApiTestCase */ private function createShortUrl(array $body = []): array { - $body['longUrl'] = 'https://app.shlink.io'; + if (! isset($body['longUrl'])) { + $body['longUrl'] = 'https://app.shlink.io'; + } $resp = $this->callApiWithKey(self::METHOD_POST, '/short-urls', [RequestOptions::JSON => $body]); $payload = $this->getJsonResponsePayload($resp);