shlink/module/Rest/test-api/Action/CreateShortUrlTest.php

371 lines
14 KiB
PHP
Raw Normal View History

<?php
2019-10-05 18:26:10 +03:00
declare(strict_types=1);
namespace ShlinkioApiTest\Shlink\Rest\Action;
use Cake\Chronos\Chronos;
use GuzzleHttp\RequestOptions;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
2019-08-11 17:30:46 +03:00
use Shlinkio\Shlink\TestUtils\ApiTest\ApiTestCase;
2019-02-17 22:28:34 +03:00
use function Functional\map;
use function range;
use function sprintf;
2021-01-24 11:25:36 +03:00
class CreateShortUrlTest extends ApiTestCase
{
#[Test]
public function createsNewShortUrlWhenOnlyLongUrlIsProvided(): void
{
$expectedKeys = ['shortCode', 'shortUrl', 'longUrl', 'dateCreated', 'visitsCount', 'tags'];
[$statusCode, $payload] = $this->createShortUrl();
2020-10-04 01:35:14 +03:00
self::assertEquals(self::STATUS_OK, $statusCode);
foreach ($expectedKeys as $key) {
2020-10-04 01:35:14 +03:00
self::assertArrayHasKey($key, $payload);
}
}
#[Test]
public function createsNewShortUrlWithCustomSlug(): void
{
[$statusCode, $payload] = $this->createShortUrl(['customSlug' => 'my cool slug']);
2020-10-04 01:35:14 +03:00
self::assertEquals(self::STATUS_OK, $statusCode);
self::assertEquals('my-cool-slug', $payload['shortCode']);
}
#[Test, DataProvider('provideConflictingSlugs')]
public function failsToCreateShortUrlWithDuplicatedSlug(string $slug, ?string $domain): void
{
$suffix = $domain === null ? '' : sprintf(' for domain "%s"', $domain);
$detail = sprintf('Provided slug "%s" is already in use%s.', $slug, $suffix);
[$statusCode, $payload] = $this->createShortUrl(['customSlug' => $slug, 'domain' => $domain]);
2020-10-04 01:35:14 +03:00
self::assertEquals(self::STATUS_BAD_REQUEST, $statusCode);
self::assertEquals(self::STATUS_BAD_REQUEST, $payload['status']);
self::assertEquals($detail, $payload['detail']);
self::assertEquals('INVALID_SLUG', $payload['type']);
self::assertEquals('Invalid custom slug', $payload['title']);
self::assertEquals($slug, $payload['customSlug']);
if ($domain !== null) {
2020-10-04 01:35:14 +03:00
self::assertEquals($domain, $payload['domain']);
} else {
2020-10-04 01:35:14 +03:00
self::assertArrayNotHasKey('domain', $payload);
}
}
#[Test, DataProvider('provideDuplicatedSlugApiVersions')]
2022-08-13 18:48:55 +03:00
public function expectedTypeIsReturnedForConflictingSlugBasedOnApiVersion(
string $version,
string $expectedType,
): void {
[, $payload] = $this->createShortUrl(['customSlug' => 'custom'], version: $version);
self::assertEquals($expectedType, $payload['type']);
}
2023-02-09 11:32:38 +03:00
public static function provideDuplicatedSlugApiVersions(): iterable
2022-08-13 18:48:55 +03:00
{
yield ['1', 'INVALID_SLUG'];
yield ['2', 'INVALID_SLUG'];
yield ['3', 'https://shlink.io/api/error/non-unique-slug'];
}
#[Test, DataProvider('provideTags')]
public function createsNewShortUrlWithTags(array $providedTags, array $expectedTags): void
{
[$statusCode, ['tags' => $tags]] = $this->createShortUrl(['tags' => $providedTags]);
2020-10-04 01:35:14 +03:00
self::assertEquals(self::STATUS_OK, $statusCode);
self::assertEquals($expectedTags, $tags);
}
2023-02-09 11:32:38 +03:00
public static function provideTags(): iterable
{
yield 'simple tags' => [$simpleTags = ['foo', 'bar', 'baz'], $simpleTags];
yield 'tags with spaces' => [['fo o', ' bar', 'b az'], ['fo-o', 'bar', 'b-az']];
yield 'tags with special chars' => [['UUU', 'Aäa'], ['uuu', 'aäa']];
}
#[Test, DataProvider('provideMaxVisits')]
public function createsNewShortUrlWithVisitsLimit(int $maxVisits): void
{
[$statusCode, ['shortCode' => $shortCode]] = $this->createShortUrl(['maxVisits' => $maxVisits]);
2020-10-04 01:35:14 +03:00
self::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++) {
2020-10-04 01:35:14 +03:00
self::assertEquals(self::STATUS_FOUND, $this->callShortUrl($shortCode)->getStatusCode());
}
$lastResp = $this->callShortUrl($shortCode);
2020-10-04 01:35:14 +03:00
self::assertEquals(self::STATUS_NOT_FOUND, $lastResp->getStatusCode());
}
2023-02-09 11:32:38 +03:00
public static function provideMaxVisits(): array
{
return map(range(10, 15), fn(int $i) => [$i]);
}
#[Test]
public function createsShortUrlWithValidSince(): void
{
[$statusCode, ['shortCode' => $shortCode]] = $this->createShortUrl([
2023-08-19 12:48:04 +03:00
'validSince' => Chronos::now()->addDays(1)->toAtomString(),
]);
2020-10-04 01:35:14 +03:00
self::assertEquals(self::STATUS_OK, $statusCode);
// Request to the short URL will return a 404 since it's not valid yet
$lastResp = $this->callShortUrl($shortCode);
2020-10-04 01:35:14 +03:00
self::assertEquals(self::STATUS_NOT_FOUND, $lastResp->getStatusCode());
}
#[Test]
public function createsShortUrlWithValidUntil(): void
{
[$statusCode, ['shortCode' => $shortCode]] = $this->createShortUrl([
2023-08-19 12:48:04 +03:00
'validUntil' => Chronos::now()->subDays(1)->toAtomString(),
]);
2020-10-04 01:35:14 +03:00
self::assertEquals(self::STATUS_OK, $statusCode);
// Request to the short URL will return a 404 since it's no longer valid
$lastResp = $this->callShortUrl($shortCode);
2020-10-04 01:35:14 +03:00
self::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);
2020-10-04 01:35:14 +03:00
self::assertEquals(self::STATUS_OK, $firstStatusCode);
self::assertEquals(self::STATUS_OK, $secondStatusCode);
self::assertEquals($firstShortCode, $secondShortCode);
}
2023-02-09 11:32:38 +03:00
public static function provideMatchingBodies(): iterable
{
$longUrl = 'https://www.alejandrocelaya.com';
2019-02-17 22:28:34 +03:00
yield 'only long URL' => [['longUrl' => $longUrl]];
yield 'long URL and tags' => [['longUrl' => $longUrl, 'tags' => ['boo', 'far']]];
yield 'long URL and custom slug' => [['longUrl' => $longUrl, 'customSlug' => 'my cool slug']];
yield 'several params' => [
[
'longUrl' => $longUrl,
'tags' => ['boo', 'far'],
'validSince' => Chronos::now()->toAtomString(),
'maxVisits' => 7,
],
];
}
#[Test, DataProvider('provideConflictingSlugs')]
public function returnsErrorWhenRequestingReturnExistingButCustomSlugIsInUse(string $slug, ?string $domain): void
{
$longUrl = 'https://www.alejandrocelaya.com';
[$firstStatusCode] = $this->createShortUrl(['longUrl' => $longUrl]);
[$secondStatusCode] = $this->createShortUrl([
'longUrl' => $longUrl,
'customSlug' => $slug,
'findIfExists' => true,
'domain' => $domain,
]);
2020-10-04 01:35:14 +03:00
self::assertEquals(self::STATUS_OK, $firstStatusCode);
self::assertEquals(self::STATUS_BAD_REQUEST, $secondStatusCode);
}
2023-02-09 11:32:38 +03:00
public static function provideConflictingSlugs(): iterable
{
yield 'without domain' => ['custom', null];
yield 'with domain' => ['custom-with-domain', 'some-domain.com'];
}
#[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,
]);
2020-10-04 01:35:14 +03:00
self::assertEquals(self::STATUS_OK, $firstStatusCode);
self::assertEquals(self::STATUS_OK, $secondStatusCode);
self::assertNotEquals($firstShortCode, $secondShortCode);
}
#[Test, DataProvider('provideIdn')]
public function createsNewShortUrlWithInternationalizedDomainName(string $longUrl): void
2019-11-16 12:06:55 +03:00
{
[$statusCode, $payload] = $this->createShortUrl(['longUrl' => $longUrl]);
2019-11-16 12:06:55 +03:00
2020-10-04 01:35:14 +03:00
self::assertEquals(self::STATUS_OK, $statusCode);
self::assertEquals($payload['longUrl'], $longUrl);
2019-11-16 12:06:55 +03:00
}
2023-02-09 11:32:38 +03:00
public static function provideIdn(): iterable
{
yield ['http://tést.shlink.io']; // Redirects to https://shlink.io
yield ['http://test.shlink.io']; // Redirects to http://tést.shlink.io
yield ['http://téstb.shlink.io']; // Redirects to http://tést.shlink.io
}
#[Test, DataProvider('provideInvalidUrls')]
2022-08-13 18:48:55 +03:00
public function failsToCreateShortUrlWithInvalidLongUrl(string $url, string $version, string $expectedType): void
{
$expectedDetail = sprintf('Provided URL %s is invalid. Try with a different one.', $url);
2022-08-13 18:48:55 +03:00
[$statusCode, $payload] = $this->createShortUrl(['longUrl' => $url, 'validateUrl' => true], version: $version);
2020-10-04 01:35:14 +03:00
self::assertEquals(self::STATUS_BAD_REQUEST, $statusCode);
self::assertEquals(self::STATUS_BAD_REQUEST, $payload['status']);
2022-08-13 18:48:55 +03:00
self::assertEquals($expectedType, $payload['type']);
2020-10-04 01:35:14 +03:00
self::assertEquals($expectedDetail, $payload['detail']);
self::assertEquals('Invalid URL', $payload['title']);
self::assertEquals($url, $payload['url']);
}
2023-02-09 11:32:38 +03:00
public static function provideInvalidUrls(): iterable
{
yield 'API version 2' => ['https://this-has-to-be-invalid.com', '2', 'INVALID_URL'];
yield 'API version 3' => ['https://this-has-to-be-invalid.com', '3', 'https://shlink.io/api/error/invalid-url'];
}
#[Test, DataProvider('provideInvalidArgumentApiVersions')]
public function failsToCreateShortUrlWithoutLongUrl(array $payload, string $version, string $expectedType): void
{
2022-08-13 18:48:55 +03:00
$resp = $this->callApiWithKey(
self::METHOD_POST,
sprintf('/rest/v%s/short-urls', $version),
[RequestOptions::JSON => $payload],
2022-08-13 18:48:55 +03:00
);
$payload = $this->getJsonResponsePayload($resp);
self::assertEquals(self::STATUS_BAD_REQUEST, $resp->getStatusCode());
self::assertEquals(self::STATUS_BAD_REQUEST, $payload['status']);
2022-08-13 18:48:55 +03:00
self::assertEquals($expectedType, $payload['type']);
self::assertEquals('Provided data is not valid', $payload['detail']);
self::assertEquals('Invalid data', $payload['title']);
}
2023-02-09 11:32:38 +03:00
public static function provideInvalidArgumentApiVersions(): iterable
2022-08-13 18:48:55 +03:00
{
yield 'missing long url v2' => [[], '2', 'INVALID_ARGUMENT'];
yield 'missing long url v3' => [[], '3', 'https://shlink.io/api/error/invalid-data'];
yield 'empty long url v2' => [['longUrl' => null], '2', 'INVALID_ARGUMENT'];
yield 'empty long url v3' => [['longUrl' => ' '], '3', 'https://shlink.io/api/error/invalid-data'];
yield 'missing url schema v2' => [['longUrl' => 'foo.com'], '2', 'INVALID_ARGUMENT'];
yield 'missing url schema v3' => [['longUrl' => 'foo.com'], '3', 'https://shlink.io/api/error/invalid-data'];
yield 'empty device long url v2' => [[
'longUrl' => 'foo',
'deviceLongUrls' => [
'android' => null,
],
], '2', 'INVALID_ARGUMENT'];
yield 'empty device long url v3' => [[
'longUrl' => 'foo',
'deviceLongUrls' => [
'ios' => ' ',
],
], '3', 'https://shlink.io/api/error/invalid-data'];
2022-08-13 18:48:55 +03:00
}
#[Test]
public function defaultDomainIsDroppedIfProvided(): void
{
[$createStatusCode, ['shortCode' => $shortCode]] = $this->createShortUrl([
'longUrl' => 'https://www.alejandrocelaya.com',
'domain' => 's.test',
]);
$getResp = $this->callApiWithKey(self::METHOD_GET, '/short-urls/' . $shortCode);
$payload = $this->getJsonResponsePayload($getResp);
2020-10-04 01:35:14 +03:00
self::assertEquals(self::STATUS_OK, $createStatusCode);
self::assertEquals(self::STATUS_OK, $getResp->getStatusCode());
self::assertArrayHasKey('domain', $payload);
self::assertNull($payload['domain']);
}
#[Test, DataProvider('provideDomains')]
public function apiKeyDomainIsEnforced(?string $providedDomain): void
{
[$statusCode, ['domain' => $returnedDomain]] = $this->createShortUrl(
['domain' => $providedDomain],
'domain_api_key',
);
self::assertEquals(self::STATUS_OK, $statusCode);
self::assertEquals('example.com', $returnedDomain);
}
2023-02-09 11:32:38 +03:00
public static function provideDomains(): iterable
{
yield 'no domain' => [null];
yield 'invalid domain' => ['this-will-be-overwritten.com'];
yield 'example domain' => ['example.com'];
}
#[Test]
2022-01-10 15:04:16 +03:00
public function canCreateShortUrlsWithEmojis(): void
{
[$statusCode, $payload] = $this->createShortUrl([
'longUrl' => 'https://emojipedia.org/fire/',
'title' => '🔥🔥🔥',
'customSlug' => '🦣🦣🦣',
]);
self::assertEquals(self::STATUS_OK, $statusCode);
self::assertEquals('🔥🔥🔥', $payload['title']);
self::assertEquals('🦣🦣🦣', $payload['shortCode']);
self::assertEquals('http://s.test/🦣🦣🦣', $payload['shortUrl']);
2022-01-10 15:04:16 +03:00
}
#[Test]
public function canCreateShortUrlsWithDeviceLongUrls(): void
{
[$statusCode, $payload] = $this->createShortUrl([
'longUrl' => 'https://github.com/shlinkio/shlink/issues/1557',
'deviceLongUrls' => [
'ios' => 'https://github.com/shlinkio/shlink/ios',
'android' => 'https://github.com/shlinkio/shlink/android',
],
]);
self::assertEquals(self::STATUS_OK, $statusCode);
self::assertEquals('https://github.com/shlinkio/shlink/ios', $payload['deviceLongUrls']['ios'] ?? null);
self::assertEquals('https://github.com/shlinkio/shlink/android', $payload['deviceLongUrls']['android'] ?? null);
}
/**
2022-10-24 21:25:06 +03:00
* @return array{int, array}
*/
2022-08-13 18:48:55 +03:00
private function createShortUrl(array $body = [], string $apiKey = 'valid_api_key', string $version = '2'): array
{
if (! isset($body['longUrl'])) {
$body['longUrl'] = 'https://app.shlink.io';
}
2022-08-13 18:48:55 +03:00
$resp = $this->callApiWithKey(
self::METHOD_POST,
sprintf('/rest/v%s/short-urls', $version),
[RequestOptions::JSON => $body],
$apiKey,
);
$payload = $this->getJsonResponsePayload($resp);
return [$resp->getStatusCode(), $payload];
}
}