123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334 |
- <?php
- declare(strict_types=1);
- namespace ShlinkioApiTest\Shlink\Rest\Action;
- use Cake\Chronos\Chronos;
- use GuzzleHttp\RequestOptions;
- use Shlinkio\Shlink\TestUtils\ApiTest\ApiTestCase;
- use function Functional\map;
- use function range;
- use function sprintf;
- class CreateShortUrlTest extends ApiTestCase
- {
- /** @test */
- public function createsNewShortUrlWhenOnlyLongUrlIsProvided(): void
- {
- $expectedKeys = ['shortCode', 'shortUrl', 'longUrl', 'dateCreated', 'visitsCount', 'tags'];
- [$statusCode, $payload] = $this->createShortUrl();
- self::assertEquals(self::STATUS_OK, $statusCode);
- foreach ($expectedKeys as $key) {
- self::assertArrayHasKey($key, $payload);
- }
- }
- /** @test */
- public function createsNewShortUrlWithCustomSlug(): void
- {
- [$statusCode, $payload] = $this->createShortUrl(['customSlug' => 'my cool slug']);
- 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]);
- 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) {
- self::assertEquals($domain, $payload['domain']);
- } else {
- self::assertArrayNotHasKey('domain', $payload);
- }
- }
- /**
- * @test
- * @dataProvider provideTags
- */
- public function createsNewShortUrlWithTags(array $providedTags, array $expectedTags): void
- {
- [$statusCode, ['tags' => $tags]] = $this->createShortUrl(['tags' => $providedTags]);
- self::assertEquals(self::STATUS_OK, $statusCode);
- self::assertEquals($expectedTags, $tags);
- }
- public 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]);
- 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++) {
- self::assertEquals(self::STATUS_FOUND, $this->callShortUrl($shortCode)->getStatusCode());
- }
- $lastResp = $this->callShortUrl($shortCode);
- self::assertEquals(self::STATUS_NOT_FOUND, $lastResp->getStatusCode());
- }
- public function provideMaxVisits(): array
- {
- return map(range(10, 15), fn (int $i) => [$i]);
- }
- /** @test */
- public function createsShortUrlWithValidSince(): void
- {
- [$statusCode, ['shortCode' => $shortCode]] = $this->createShortUrl([
- 'validSince' => Chronos::now()->addDay()->toAtomString(),
- ]);
- 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);
- self::assertEquals(self::STATUS_NOT_FOUND, $lastResp->getStatusCode());
- }
- /** @test */
- public function createsShortUrlWithValidUntil(): void
- {
- [$statusCode, ['shortCode' => $shortCode]] = $this->createShortUrl([
- 'validUntil' => Chronos::now()->subDay()->toAtomString(),
- ]);
- 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);
- 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);
- self::assertEquals(self::STATUS_OK, $firstStatusCode);
- self::assertEquals(self::STATUS_OK, $secondStatusCode);
- self::assertEquals($firstShortCode, $secondShortCode);
- }
- public function provideMatchingBodies(): iterable
- {
- $longUrl = 'https://www.alejandrocelaya.com';
- 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,
- ]);
- self::assertEquals(self::STATUS_OK, $firstStatusCode);
- self::assertEquals(self::STATUS_BAD_REQUEST, $secondStatusCode);
- }
- public 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,
- ]);
- 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
- {
- [$statusCode, $payload] = $this->createShortUrl(['longUrl' => $longUrl]);
- self::assertEquals(self::STATUS_OK, $statusCode);
- self::assertEquals($payload['longUrl'], $longUrl);
- }
- public 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
- */
- public function failsToCreateShortUrlWithInvalidLongUrl(string $url): void
- {
- $expectedDetail = sprintf('Provided URL %s is invalid. Try with a different one.', $url);
- [$statusCode, $payload] = $this->createShortUrl(['longUrl' => $url]);
- self::assertEquals(self::STATUS_BAD_REQUEST, $statusCode);
- self::assertEquals(self::STATUS_BAD_REQUEST, $payload['status']);
- self::assertEquals('INVALID_URL', $payload['type']);
- self::assertEquals($expectedDetail, $payload['detail']);
- self::assertEquals('Invalid URL', $payload['title']);
- self::assertEquals($url, $payload['url']);
- }
- public function provideInvalidUrls(): iterable
- {
- yield 'empty URL' => [''];
- yield 'non-reachable URL' => ['https://this-has-to-be-invalid.com'];
- }
- /** @test */
- public function failsToCreateShortUrlWithoutLongUrl(): void
- {
- $resp = $this->callApiWithKey(self::METHOD_POST, '/short-urls', [RequestOptions::JSON => []]);
- $payload = $this->getJsonResponsePayload($resp);
- self::assertEquals(self::STATUS_BAD_REQUEST, $resp->getStatusCode());
- self::assertEquals(self::STATUS_BAD_REQUEST, $payload['status']);
- self::assertEquals('INVALID_ARGUMENT', $payload['type']);
- self::assertEquals('Provided data is not valid', $payload['detail']);
- self::assertEquals('Invalid data', $payload['title']);
- }
- /** @test */
- public function defaultDomainIsDroppedIfProvided(): void
- {
- [$createStatusCode, ['shortCode' => $shortCode]] = $this->createShortUrl([
- 'longUrl' => 'https://www.alejandrocelaya.com',
- 'domain' => 'doma.in',
- ]);
- $getResp = $this->callApiWithKey(self::METHOD_GET, '/short-urls/' . $shortCode);
- $payload = $this->getJsonResponsePayload($getResp);
- 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);
- }
- public function provideDomains(): iterable
- {
- yield 'no domain' => [null];
- yield 'invalid domain' => ['this-will-be-overwritten.com'];
- yield 'example domain' => ['example.com'];
- }
- /**
- * @test
- * @dataProvider provideTwitterUrls
- */
- public function urlsWithBothProtectionCanBeShortenedWithUrlValidationEnabled(string $longUrl): void
- {
- [$statusCode] = $this->createShortUrl(['longUrl' => $longUrl, 'validateUrl' => true]);
- self::assertEquals(self::STATUS_OK, $statusCode);
- }
- public function provideTwitterUrls(): iterable
- {
- yield ['https://twitter.com/shlinkio'];
- yield ['https://mobile.twitter.com/shlinkio'];
- yield ['https://twitter.com/shlinkio/status/1360637738421268481'];
- yield ['https://mobile.twitter.com/shlinkio/status/1360637738421268481'];
- }
- /**
- * @return array {
- * @var int $statusCode
- * @var array $payload
- * }
- */
- private function createShortUrl(array $body = [], string $apiKey = 'valid_api_key'): array
- {
- if (! isset($body['longUrl'])) {
- $body['longUrl'] = 'https://app.shlink.io';
- }
- $resp = $this->callApiWithKey(self::METHOD_POST, '/short-urls', [RequestOptions::JSON => $body], $apiKey);
- $payload = $this->getJsonResponsePayload($resp);
- return [$resp->getStatusCode(), $payload];
- }
- }
|