2019-01-30 11:28:07 -06:00
|
|
|
<?php
|
2019-10-05 10:26:10 -05:00
|
|
|
|
2019-01-30 11:28:07 -06:00
|
|
|
declare(strict_types=1);
|
|
|
|
|
|
|
|
namespace ShlinkioApiTest\Shlink\Rest\Action;
|
|
|
|
|
|
|
|
use Cake\Chronos\Chronos;
|
|
|
|
use GuzzleHttp\RequestOptions;
|
2019-08-11 09:30:46 -05:00
|
|
|
use Shlinkio\Shlink\TestUtils\ApiTest\ApiTestCase;
|
2019-02-26 15:56:43 -06:00
|
|
|
|
2019-02-17 13:28:34 -06:00
|
|
|
use function Functional\map;
|
|
|
|
use function range;
|
2019-11-27 13:48:35 -06:00
|
|
|
use function sprintf;
|
2019-01-30 11:28:07 -06:00
|
|
|
|
|
|
|
class CreateShortUrlActionTest extends ApiTestCase
|
|
|
|
{
|
2019-02-17 13:28:34 -06:00
|
|
|
/** @test */
|
2019-02-03 04:01:38 -06:00
|
|
|
public function createsNewShortUrlWhenOnlyLongUrlIsProvided(): void
|
2019-01-30 11:28:07 -06:00
|
|
|
{
|
|
|
|
$expectedKeys = ['shortCode', 'shortUrl', 'longUrl', 'dateCreated', 'visitsCount', 'tags'];
|
|
|
|
[$statusCode, $payload] = $this->createShortUrl();
|
|
|
|
|
|
|
|
$this->assertEquals(self::STATUS_OK, $statusCode);
|
|
|
|
foreach ($expectedKeys as $key) {
|
|
|
|
$this->assertArrayHasKey($key, $payload);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-02-17 13:28:34 -06:00
|
|
|
/** @test */
|
2019-02-03 04:01:38 -06:00
|
|
|
public function createsNewShortUrlWithCustomSlug(): void
|
2019-01-30 11:28:07 -06:00
|
|
|
{
|
|
|
|
[$statusCode, $payload] = $this->createShortUrl(['customSlug' => 'my cool slug']);
|
|
|
|
|
|
|
|
$this->assertEquals(self::STATUS_OK, $statusCode);
|
|
|
|
$this->assertEquals('my-cool-slug', $payload['shortCode']);
|
|
|
|
}
|
|
|
|
|
2019-10-02 13:13:25 -05:00
|
|
|
/**
|
|
|
|
* @test
|
|
|
|
* @dataProvider provideConflictingSlugs
|
|
|
|
*/
|
|
|
|
public function failsToCreateShortUrlWithDuplicatedSlug(string $slug, ?string $domain): void
|
|
|
|
{
|
2019-11-27 13:48:35 -06:00
|
|
|
$suffix = $domain === null ? '' : sprintf(' for domain "%s"', $domain);
|
|
|
|
$detail = sprintf('Provided slug "%s" is already in use%s.', $slug, $suffix);
|
|
|
|
|
2019-10-02 13:13:25 -05:00
|
|
|
[$statusCode, $payload] = $this->createShortUrl(['customSlug' => $slug, 'domain' => $domain]);
|
|
|
|
|
|
|
|
$this->assertEquals(self::STATUS_BAD_REQUEST, $statusCode);
|
2019-11-27 13:48:35 -06:00
|
|
|
$this->assertEquals(self::STATUS_BAD_REQUEST, $payload['status']);
|
|
|
|
$this->assertEquals($detail, $payload['detail']);
|
|
|
|
$this->assertEquals($detail, $payload['message']); // Deprecated
|
|
|
|
$this->assertEquals('INVALID_SLUG', $payload['type']);
|
|
|
|
$this->assertEquals('INVALID_SLUG', $payload['error']); // Deprecated
|
|
|
|
$this->assertEquals('Invalid custom slug', $payload['title']);
|
|
|
|
$this->assertEquals($slug, $payload['customSlug']);
|
|
|
|
|
|
|
|
if ($domain !== null) {
|
|
|
|
$this->assertEquals($domain, $payload['domain']);
|
|
|
|
} else {
|
|
|
|
$this->assertArrayNotHasKey('domain', $payload);
|
|
|
|
}
|
2019-10-02 13:13:25 -05:00
|
|
|
}
|
|
|
|
|
2019-02-17 13:28:34 -06:00
|
|
|
/** @test */
|
2019-02-03 04:01:38 -06:00
|
|
|
public function createsNewShortUrlWithTags(): void
|
2019-01-30 11:28:07 -06:00
|
|
|
{
|
2019-11-01 03:52:56 -05:00
|
|
|
[$statusCode, ['tags' => $tags]] = $this->createShortUrl(['tags' => ['foo', 'bar', 'baz']]);
|
2019-01-30 11:28:07 -06:00
|
|
|
|
|
|
|
$this->assertEquals(self::STATUS_OK, $statusCode);
|
2019-11-01 03:52:56 -05:00
|
|
|
$this->assertEquals(['foo', 'bar', 'baz'], $tags);
|
2019-01-30 11:28:07 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @test
|
|
|
|
* @dataProvider provideMaxVisits
|
|
|
|
*/
|
2019-02-03 04:01:38 -06:00
|
|
|
public function createsNewShortUrlWithVisitsLimit(int $maxVisits): void
|
2019-01-30 11:28:07 -06:00
|
|
|
{
|
|
|
|
[$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
|
|
|
|
{
|
2019-11-01 11:16:56 -05:00
|
|
|
return map(range(10, 15), function (int $i) {
|
2019-02-17 13:28:34 -06:00
|
|
|
return [$i];
|
|
|
|
});
|
2019-01-30 11:28:07 -06:00
|
|
|
}
|
|
|
|
|
2019-02-17 13:28:34 -06:00
|
|
|
/** @test */
|
2019-02-03 04:01:38 -06:00
|
|
|
public function createsShortUrlWithValidSince(): void
|
2019-01-30 11:28:07 -06:00
|
|
|
{
|
|
|
|
[$statusCode, ['shortCode' => $shortCode]] = $this->createShortUrl([
|
|
|
|
'validSince' => Chronos::now()->addDay()->toAtomString(),
|
|
|
|
]);
|
|
|
|
|
|
|
|
$this->assertEquals(self::STATUS_OK, $statusCode);
|
|
|
|
|
2019-10-11 02:14:25 -05:00
|
|
|
// Request to the short URL will return a 404 since it's not valid yet
|
2019-01-30 11:28:07 -06:00
|
|
|
$lastResp = $this->callShortUrl($shortCode);
|
|
|
|
$this->assertEquals(self::STATUS_NOT_FOUND, $lastResp->getStatusCode());
|
|
|
|
}
|
|
|
|
|
2019-02-17 13:28:34 -06:00
|
|
|
/** @test */
|
2019-02-03 04:01:38 -06:00
|
|
|
public function createsShortUrlWithValidUntil(): void
|
2019-01-30 11:28:07 -06:00
|
|
|
{
|
|
|
|
[$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());
|
|
|
|
}
|
|
|
|
|
2019-02-03 04:01:38 -06:00
|
|
|
/**
|
|
|
|
* @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);
|
|
|
|
}
|
|
|
|
|
2019-02-17 13:28:34 -06:00
|
|
|
public function provideMatchingBodies(): iterable
|
2019-02-03 04:01:38 -06:00
|
|
|
{
|
|
|
|
$longUrl = 'https://www.alejandrocelaya.com';
|
|
|
|
|
2019-02-17 13:28:34 -06: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,
|
|
|
|
]];
|
2019-02-03 04:01:38 -06:00
|
|
|
}
|
|
|
|
|
2019-10-02 13:13:25 -05:00
|
|
|
/**
|
|
|
|
* @test
|
|
|
|
* @dataProvider provideConflictingSlugs
|
|
|
|
*/
|
|
|
|
public function returnsErrorWhenRequestingReturnExistingButCustomSlugIsInUse(string $slug, ?string $domain): void
|
2019-02-03 04:01:38 -06:00
|
|
|
{
|
|
|
|
$longUrl = 'https://www.alejandrocelaya.com';
|
|
|
|
|
|
|
|
[$firstStatusCode] = $this->createShortUrl(['longUrl' => $longUrl]);
|
|
|
|
[$secondStatusCode] = $this->createShortUrl([
|
|
|
|
'longUrl' => $longUrl,
|
2019-10-02 13:13:25 -05:00
|
|
|
'customSlug' => $slug,
|
2019-02-03 04:01:38 -06:00
|
|
|
'findIfExists' => true,
|
2019-10-02 13:13:25 -05:00
|
|
|
'domain' => $domain,
|
2019-02-03 04:01:38 -06:00
|
|
|
]);
|
|
|
|
|
|
|
|
$this->assertEquals(self::STATUS_OK, $firstStatusCode);
|
|
|
|
$this->assertEquals(self::STATUS_BAD_REQUEST, $secondStatusCode);
|
|
|
|
}
|
|
|
|
|
2019-10-02 13:13:25 -05:00
|
|
|
public function provideConflictingSlugs(): iterable
|
|
|
|
{
|
|
|
|
yield 'without domain' => ['custom', null];
|
|
|
|
yield 'with domain' => ['custom-with-domain', 'some-domain.com'];
|
|
|
|
}
|
|
|
|
|
2019-02-17 13:28:34 -06:00
|
|
|
/** @test */
|
2019-02-03 04:01:38 -06:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2019-11-16 05:38:45 -06:00
|
|
|
/**
|
|
|
|
* @test
|
|
|
|
* @dataProvider provideIdn
|
|
|
|
*/
|
|
|
|
public function createsNewShortUrlWithInternationalizedDomainName(string $longUrl): void
|
2019-11-16 03:06:55 -06:00
|
|
|
{
|
2019-11-16 06:37:53 -06:00
|
|
|
[$statusCode, $payload] = $this->createShortUrl(['longUrl' => $longUrl]);
|
2019-11-16 03:06:55 -06:00
|
|
|
|
|
|
|
$this->assertEquals(self::STATUS_OK, $statusCode);
|
2019-11-16 06:37:53 -06:00
|
|
|
$this->assertEquals($payload['longUrl'], $longUrl);
|
2019-11-16 03:06:55 -06:00
|
|
|
}
|
|
|
|
|
2019-11-16 05:38:45 -06:00
|
|
|
public function provideIdn(): iterable
|
|
|
|
{
|
2019-11-16 06:37:53 -06:00
|
|
|
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
|
2019-11-16 05:38:45 -06:00
|
|
|
}
|
|
|
|
|
2019-11-26 14:33:22 -06:00
|
|
|
/** @test */
|
|
|
|
public function failsToCreateShortUrlWithInvalidOriginalUrl(): void
|
|
|
|
{
|
2019-11-27 13:48:35 -06:00
|
|
|
$url = 'https://this-has-to-be-invalid.com';
|
|
|
|
$expectedDetail = sprintf('Provided URL %s is invalid. Try with a different one.', $url);
|
|
|
|
|
|
|
|
[$statusCode, $payload] = $this->createShortUrl(['longUrl' => $url]);
|
2019-11-26 14:33:22 -06:00
|
|
|
|
|
|
|
$this->assertEquals(self::STATUS_BAD_REQUEST, $statusCode);
|
2019-11-27 13:48:35 -06:00
|
|
|
$this->assertEquals(self::STATUS_BAD_REQUEST, $payload['status']);
|
|
|
|
$this->assertEquals('INVALID_URL', $payload['type']);
|
|
|
|
$this->assertEquals('INVALID_URL', $payload['error']); // Deprecated
|
|
|
|
$this->assertEquals($expectedDetail, $payload['detail']);
|
|
|
|
$this->assertEquals($expectedDetail, $payload['message']); // Deprecated
|
|
|
|
$this->assertEquals('Invalid URL', $payload['title']);
|
|
|
|
$this->assertEquals($url, $payload['url']);
|
2019-11-26 14:33:22 -06:00
|
|
|
}
|
|
|
|
|
2019-01-30 11:28:07 -06:00
|
|
|
/**
|
|
|
|
* @return array {
|
|
|
|
* @var int $statusCode
|
|
|
|
* @var array $payload
|
|
|
|
* }
|
|
|
|
*/
|
|
|
|
private function createShortUrl(array $body = []): array
|
|
|
|
{
|
2019-02-03 04:01:38 -06:00
|
|
|
if (! isset($body['longUrl'])) {
|
|
|
|
$body['longUrl'] = 'https://app.shlink.io';
|
|
|
|
}
|
2019-01-30 11:28:07 -06:00
|
|
|
$resp = $this->callApiWithKey(self::METHOD_POST, '/short-urls', [RequestOptions::JSON => $body]);
|
|
|
|
$payload = $this->getJsonResponsePayload($resp);
|
|
|
|
|
|
|
|
return [$resp->getStatusCode(), $payload];
|
|
|
|
}
|
|
|
|
}
|