diff --git a/config/test/bootstrap_api_tests.php b/config/test/bootstrap_api_tests.php index c39042fa..a016f93f 100644 --- a/config/test/bootstrap_api_tests.php +++ b/config/test/bootstrap_api_tests.php @@ -17,10 +17,10 @@ if (! file_exists('.env')) { $container = require __DIR__ . '/../container.php'; $testHelper = $container->get(TestHelper::class); $config = $container->get('config'); +$em = $container->get(EntityManager::class); $testHelper->createTestDb(); - -$em = $container->get(EntityManager::class); -$testHelper->seedFixtures($em, $config['data_fixtures'] ?? []); - 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/test_config.global.php b/config/test/test_config.global.php index 10778bf7..dd794fe4 100644 --- a/config/test/test_config.global.php +++ b/config/test/test_config.global.php @@ -40,6 +40,7 @@ return [ 'services' => [ 'shlink_test_api_client' => new Client([ 'base_uri' => sprintf('http://%s:%s/', $swooleTestingHost, $swooleTestingPort), + 'http_errors' => false, ]), ], 'factories' => [ diff --git a/module/Common/test-db/ApiTest/ApiTestCase.php b/module/Common/test-db/ApiTest/ApiTestCase.php index 29959837..65006bb0 100644 --- a/module/Common/test-db/ApiTest/ApiTestCase.php +++ b/module/Common/test-db/ApiTest/ApiTestCase.php @@ -6,6 +6,7 @@ namespace ShlinkioTest\Shlink\Common\ApiTest; use Fig\Http\Message\RequestMethodInterface; use Fig\Http\Message\StatusCodeInterface; use GuzzleHttp\ClientInterface; +use GuzzleHttp\RequestOptions; use PHPUnit\Framework\TestCase; use Psr\Http\Message\ResponseInterface; use Shlinkio\Shlink\Rest\Authentication\Plugin\ApiKeyHeaderPlugin; @@ -14,32 +15,40 @@ use function sprintf; abstract class ApiTestCase extends TestCase implements StatusCodeInterface, RequestMethodInterface { - private const PATH_PREFX = '/rest/v1'; + private const REST_PATH_PREFX = '/rest/v1'; /** @var ClientInterface */ private static $client; + /** @var callable */ + private static $seedFixtures; public static function setApiClient(ClientInterface $client): void { self::$client = $client; } - /** - * @throws \GuzzleHttp\Exception\GuzzleException - */ - protected function callApi(string $method, string $uri, array $options = []): ResponseInterface + public static function setSeedFixturesCallback(callable $seedFixtures): void { - return self::$client->request($method, sprintf('%s%s', self::PATH_PREFX, $uri), $options); + self::$seedFixtures = $seedFixtures; + } + + public function setUp(): void + { + if (self::$seedFixtures) { + (self::$seedFixtures)(); + } + } + + protected function callApi(string $method, string $uri, array $options = []): ResponseInterface + { + return self::$client->request($method, sprintf('%s%s', self::REST_PATH_PREFX, $uri), $options); } - /** - * @throws \GuzzleHttp\Exception\GuzzleException - */ protected function callApiWithKey(string $method, string $uri, array $options = []): ResponseInterface { - $headers = $options['headers'] ?? []; + $headers = $options[RequestOptions::HEADERS] ?? []; $headers[ApiKeyHeaderPlugin::HEADER_NAME] = 'valid_api_key'; - $options['headers'] = $headers; + $options[RequestOptions::HEADERS] = $headers; return $this->callApi($method, $uri, $options); } @@ -48,4 +57,11 @@ abstract class ApiTestCase extends TestCase implements StatusCodeInterface, Requ { return json_decode((string) $resp->getBody()); } + + protected function callShortUrl(string $shortCode): ResponseInterface + { + return self::$client->request(self::METHOD_GET, sprintf('/%s', $shortCode), [ + RequestOptions::ALLOW_REDIRECTS => false, + ]); + } } diff --git a/module/Common/test-db/TestHelper.php b/module/Common/test-db/TestHelper.php index fc24657d..fba79a58 100644 --- a/module/Common/test-db/TestHelper.php +++ b/module/Common/test-db/TestHelper.php @@ -5,6 +5,7 @@ namespace ShlinkioTest\Shlink\Common; use Doctrine\Common\DataFixtures\Executor\ORMExecutor; use Doctrine\Common\DataFixtures\Loader; +use Doctrine\Common\DataFixtures\Purger\ORMPurger; use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\Process\Process; use function file_exists; @@ -38,7 +39,7 @@ class TestHelper $loader->loadFromDirectory($path); } - $executor = new ORMExecutor($em); - $executor->execute($loader->getFixtures(), true); + $executor = new ORMExecutor($em, new ORMPurger()); + $executor->execute($loader->getFixtures()); } } diff --git a/module/Rest/test-api/Action/CreateShortUrlActionTest.php b/module/Rest/test-api/Action/CreateShortUrlActionTest.php new file mode 100644 index 00000000..2307d657 --- /dev/null +++ b/module/Rest/test-api/Action/CreateShortUrlActionTest.php @@ -0,0 +1,121 @@ +createShortUrl(); + + $this->assertEquals(self::STATUS_OK, $statusCode); + foreach ($expectedKeys as $key) { + $this->assertArrayHasKey($key, $payload); + } + } + + /** + * @test + */ + public function createsNewShortUrlWithCustomSlug() + { + [$statusCode, $payload] = $this->createShortUrl(['customSlug' => 'my cool slug']); + + $this->assertEquals(self::STATUS_OK, $statusCode); + $this->assertEquals('my-cool-slug', $payload['shortCode']); + } + + /** + * @test + */ + public function createsNewShortUrlWithTags() + { + [$statusCode, $payload] = $this->createShortUrl(['tags' => ['foo', 'bar', 'baz']]); + + $this->assertEquals(self::STATUS_OK, $statusCode); + $this->assertEquals(['foo', 'bar', 'baz'], $payload['tags']); + } + + /** + * @test + * @dataProvider provideMaxVisits + */ + public function createsNewShortUrlWithVisitsLimit(int $maxVisits) + { + [$statusCode, ['shortCode' => $shortCode]] = $this->createShortUrl(['maxVisits' => $maxVisits]); + + $this->assertEquals(self::STATUS_OK, $statusCode); + + // Last request to the short URL will return a 404, and the rest, a 302 + for ($i = 0; $i < $maxVisits; $i++) { + $this->assertEquals(self::STATUS_FOUND, $this->callShortUrl($shortCode)->getStatusCode()); + } + $lastResp = $this->callShortUrl($shortCode); + $this->assertEquals(self::STATUS_NOT_FOUND, $lastResp->getStatusCode()); + } + + public function provideMaxVisits(): array + { + return [ + [1], + [5], + [3], + ]; + } + + /** + * @test + */ + public function createsShortUrlWithValidSince() + { + [$statusCode, ['shortCode' => $shortCode]] = $this->createShortUrl([ + 'validSince' => Chronos::now()->addDay()->toAtomString(), + ]); + + $this->assertEquals(self::STATUS_OK, $statusCode); + + // Request to the short URL will return a 404 since ist' not valid yet + $lastResp = $this->callShortUrl($shortCode); + $this->assertEquals(self::STATUS_NOT_FOUND, $lastResp->getStatusCode()); + } + + /** + * @test + */ + public function createsShortUrlWithValidUntil() + { + [$statusCode, ['shortCode' => $shortCode]] = $this->createShortUrl([ + 'validUntil' => Chronos::now()->subDay()->toAtomString(), + ]); + + $this->assertEquals(self::STATUS_OK, $statusCode); + + // Request to the short URL will return a 404 since it's no longer valid + $lastResp = $this->callShortUrl($shortCode); + $this->assertEquals(self::STATUS_NOT_FOUND, $lastResp->getStatusCode()); + } + + /** + * @return array { + * @var int $statusCode + * @var array $payload + * } + */ + private function createShortUrl(array $body = []): array + { + $body['longUrl'] = 'https://app.shlink.io'; + $resp = $this->callApiWithKey(self::METHOD_POST, '/short-urls', [RequestOptions::JSON => $body]); + $payload = $this->getJsonResponsePayload($resp); + + return [$resp->getStatusCode(), $payload]; + } +} diff --git a/module/Rest/test-api/Middleware/AuthenticationTest.php b/module/Rest/test-api/Middleware/AuthenticationTest.php index c87b3c01..09ea3506 100644 --- a/module/Rest/test-api/Middleware/AuthenticationTest.php +++ b/module/Rest/test-api/Middleware/AuthenticationTest.php @@ -3,13 +3,11 @@ declare(strict_types=1); namespace ShlinkioApiTest\Shlink\Rest\Middleware; -use GuzzleHttp\Exception\ClientException; use Shlinkio\Shlink\Rest\Authentication\Plugin\ApiKeyHeaderPlugin; use Shlinkio\Shlink\Rest\Authentication\RequestToHttpAuthPlugin; use Shlinkio\Shlink\Rest\Util\RestUtils; use ShlinkioTest\Shlink\Common\ApiTest\ApiTestCase; use function implode; -use function Shlinkio\Shlink\Common\json_decode; use function sprintf; class AuthenticationTest extends ApiTestCase @@ -19,21 +17,18 @@ class AuthenticationTest extends ApiTestCase */ public function authorizationErrorIsReturnedIfNoApiKeyIsSent() { - try { - $this->callApi(self::METHOD_GET, '/short-codes'); - } catch (ClientException $e) { - ['error' => $error, 'message' => $message] = $this->getJsonResponsePayload($e->getResponse()); + $resp = $this->callApi(self::METHOD_GET, '/short-codes'); + ['error' => $error, 'message' => $message] = $this->getJsonResponsePayload($resp); - $this->assertEquals(self::STATUS_UNAUTHORIZED, $e->getCode()); - $this->assertEquals(RestUtils::INVALID_AUTHORIZATION_ERROR, $error); - $this->assertEquals( - sprintf( - 'Expected one of the following authentication headers, but none were provided, ["%s"]', - implode('", "', RequestToHttpAuthPlugin::SUPPORTED_AUTH_HEADERS) - ), - $message - ); - } + $this->assertEquals(self::STATUS_UNAUTHORIZED, $resp->getStatusCode()); + $this->assertEquals(RestUtils::INVALID_AUTHORIZATION_ERROR, $error); + $this->assertEquals( + sprintf( + 'Expected one of the following authentication headers, but none were provided, ["%s"]', + implode('", "', RequestToHttpAuthPlugin::SUPPORTED_AUTH_HEADERS) + ), + $message + ); } /** @@ -42,19 +37,16 @@ class AuthenticationTest extends ApiTestCase */ public function apiKeyErrorIsReturnedWhenProvidedApiKeyIsInvalid(string $apiKey) { - try { - $this->callApi(self::METHOD_GET, '/short-codes', [ - 'headers' => [ - ApiKeyHeaderPlugin::HEADER_NAME => $apiKey, - ], - ]); - } catch (ClientException $e) { - ['error' => $error, 'message' => $message] = json_decode((string) $e->getResponse()->getBody()); + $resp = $this->callApi(self::METHOD_GET, '/short-codes', [ + 'headers' => [ + ApiKeyHeaderPlugin::HEADER_NAME => $apiKey, + ], + ]); + ['error' => $error, 'message' => $message] = $this->getJsonResponsePayload($resp); - $this->assertEquals(self::STATUS_UNAUTHORIZED, $e->getCode()); - $this->assertEquals(RestUtils::INVALID_API_KEY_ERROR, $error); - $this->assertEquals('Provided API key does not exist or is invalid.', $message); - } + $this->assertEquals(self::STATUS_UNAUTHORIZED, $resp->getStatusCode()); + $this->assertEquals(RestUtils::INVALID_API_KEY_ERROR, $error); + $this->assertEquals('Provided API key does not exist or is invalid.', $message); } public function provideInvalidApiKeys(): array