mirror of
https://github.com/shlinkio/shlink.git
synced 2026-03-06 23:33:13 +08:00
Add a 3-second timeout to title resolution
This commit is contained in:
@@ -4,46 +4,144 @@ declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\Core\ShortUrl\Helper;
|
||||
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use Exception;
|
||||
use Fig\Http\Message\RequestMethodInterface;
|
||||
use GuzzleHttp\ClientInterface;
|
||||
use GuzzleHttp\RequestOptions;
|
||||
use Laminas\Diactoros\Response;
|
||||
use Laminas\Diactoros\Response\JsonResponse;
|
||||
use Laminas\Diactoros\Stream;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\MockObject\Builder\InvocationMocker;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Shlinkio\Shlink\Core\Options\UrlShortenerOptions;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlTitleResolutionHelper;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlCreation;
|
||||
use Shlinkio\Shlink\Core\Util\UrlValidatorInterface;
|
||||
|
||||
class ShortUrlTitleResolutionHelperTest extends TestCase
|
||||
{
|
||||
private ShortUrlTitleResolutionHelper $helper;
|
||||
private MockObject & UrlValidatorInterface $urlValidator;
|
||||
private const LONG_URL = 'http://foobar.com/12345/hello?foo=bar';
|
||||
|
||||
private MockObject & ClientInterface $httpClient;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->urlValidator = $this->createMock(UrlValidatorInterface::class);
|
||||
$this->helper = new ShortUrlTitleResolutionHelper($this->urlValidator);
|
||||
$this->httpClient = $this->createMock(ClientInterface::class);
|
||||
}
|
||||
|
||||
#[Test, DataProvider('provideTitles')]
|
||||
public function urlIsProperlyShortened(?string $title, int $validateWithTitleCallsNum, int $validateCallsNum): void
|
||||
#[Test]
|
||||
public function dataIsReturnedAsIsWhenResolvingTitlesIsDisabled(): void
|
||||
{
|
||||
$longUrl = 'http://foobar.com/12345/hello?foo=bar';
|
||||
$this->urlValidator->expects($this->exactly($validateWithTitleCallsNum))->method('validateUrlWithTitle')->with(
|
||||
$longUrl,
|
||||
$this->isFalse(),
|
||||
);
|
||||
$this->urlValidator->expects($this->exactly($validateCallsNum))->method('validateUrl')->with(
|
||||
$longUrl,
|
||||
$this->isFalse(),
|
||||
);
|
||||
$data = ShortUrlCreation::fromRawData(['longUrl' => self::LONG_URL]);
|
||||
$this->httpClient->expects($this->never())->method('request');
|
||||
|
||||
$this->helper->processTitleAndValidateUrl(
|
||||
ShortUrlCreation::fromRawData(['longUrl' => $longUrl, 'title' => $title]),
|
||||
$result = $this->helper()->processTitle($data);
|
||||
|
||||
self::assertSame($data, $result);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function dataIsReturnedAsIsWhenItAlreadyHasTitle(): void
|
||||
{
|
||||
$data = ShortUrlCreation::fromRawData([
|
||||
'longUrl' => self::LONG_URL,
|
||||
'title' => 'foo',
|
||||
]);
|
||||
$this->httpClient->expects($this->never())->method('request');
|
||||
|
||||
$result = $this->helper(autoResolveTitles: true)->processTitle($data);
|
||||
|
||||
self::assertSame($data, $result);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function dataIsReturnedAsIsWhenFetchingFails(): void
|
||||
{
|
||||
$data = ShortUrlCreation::fromRawData(['longUrl' => self::LONG_URL]);
|
||||
$this->expectRequestToBeCalled()->willThrowException(new Exception('Error'));
|
||||
|
||||
$result = $this->helper(autoResolveTitles: true)->processTitle($data);
|
||||
|
||||
self::assertSame($data, $result);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function dataIsReturnedAsIsWhenResponseIsNotHtml(): void
|
||||
{
|
||||
$data = ShortUrlCreation::fromRawData(['longUrl' => self::LONG_URL]);
|
||||
$this->expectRequestToBeCalled()->willReturn(new JsonResponse(['foo' => 'bar']));
|
||||
|
||||
$result = $this->helper(autoResolveTitles: true)->processTitle($data);
|
||||
|
||||
self::assertSame($data, $result);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function dataIsReturnedAsIsWhenTitleCannotBeResolvedFromResponse(): void
|
||||
{
|
||||
$data = ShortUrlCreation::fromRawData(['longUrl' => self::LONG_URL]);
|
||||
$this->expectRequestToBeCalled()->willReturn($this->respWithoutTitle());
|
||||
|
||||
$result = $this->helper(autoResolveTitles: true)->processTitle($data);
|
||||
|
||||
self::assertSame($data, $result);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function titleIsUpdatedWhenItCanBeResolvedFromResponse(): void
|
||||
{
|
||||
$data = ShortUrlCreation::fromRawData(['longUrl' => self::LONG_URL]);
|
||||
$this->expectRequestToBeCalled()->willReturn($this->respWithTitle());
|
||||
|
||||
$result = $this->helper(autoResolveTitles: true)->processTitle($data);
|
||||
|
||||
self::assertNotSame($data, $result);
|
||||
self::assertEquals('Resolved "title"', $result->title);
|
||||
}
|
||||
|
||||
private function expectRequestToBeCalled(): InvocationMocker
|
||||
{
|
||||
return $this->httpClient->expects($this->once())->method('request')->with(
|
||||
RequestMethodInterface::METHOD_GET,
|
||||
self::LONG_URL,
|
||||
[
|
||||
RequestOptions::TIMEOUT => 3,
|
||||
RequestOptions::CONNECT_TIMEOUT => 3,
|
||||
RequestOptions::ALLOW_REDIRECTS => ['max' => ShortUrlTitleResolutionHelper::MAX_REDIRECTS],
|
||||
RequestOptions::IDN_CONVERSION => true,
|
||||
RequestOptions::HEADERS => ['User-Agent' => ShortUrlTitleResolutionHelper::CHROME_USER_AGENT],
|
||||
RequestOptions::STREAM => true,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
public static function provideTitles(): iterable
|
||||
private function respWithoutTitle(): Response
|
||||
{
|
||||
yield 'no title' => [null, 1, 0];
|
||||
yield 'title' => ['link title', 0, 1];
|
||||
$body = $this->createStreamWithContent('<body>No title</body>');
|
||||
return new Response($body, 200, ['Content-Type' => 'text/html']);
|
||||
}
|
||||
|
||||
private function respWithTitle(): Response
|
||||
{
|
||||
$body = $this->createStreamWithContent('<title data-foo="bar"> Resolved "title" </title>');
|
||||
return new Response($body, 200, ['Content-Type' => 'TEXT/html; charset=utf-8']);
|
||||
}
|
||||
|
||||
private function createStreamWithContent(string $content): Stream
|
||||
{
|
||||
$body = new Stream('php://temp', 'wr');
|
||||
$body->write($content);
|
||||
$body->rewind();
|
||||
|
||||
return $body;
|
||||
}
|
||||
|
||||
private function helper(bool $autoResolveTitles = false): ShortUrlTitleResolutionHelper
|
||||
{
|
||||
return new ShortUrlTitleResolutionHelper(
|
||||
$this->httpClient,
|
||||
new UrlShortenerOptions(autoResolveTitles: $autoResolveTitles),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ class ShortUrlServiceTest extends TestCase
|
||||
)->willReturn($shortUrl);
|
||||
|
||||
$this->titleResolutionHelper->expects($expectedValidateCalls)
|
||||
->method('processTitleAndValidateUrl')
|
||||
->method('processTitle')
|
||||
->with($shortUrlEdit)
|
||||
->willReturn($shortUrlEdit);
|
||||
|
||||
@@ -102,10 +102,6 @@ class ShortUrlServiceTest extends TestCase
|
||||
'maxVisits' => 10,
|
||||
'longUrl' => 'https://modifiedLongUrl',
|
||||
]), ApiKey::create()];
|
||||
yield 'long URL with validation' => [new InvokedCount(1), ShortUrlEdition::fromRawData([
|
||||
'longUrl' => 'https://modifiedLongUrl',
|
||||
'validateUrl' => true,
|
||||
]), null];
|
||||
yield 'device redirects' => [new InvokedCount(0), ShortUrlEdition::fromRawData([
|
||||
'deviceLongUrls' => [
|
||||
DeviceType::IOS->value => 'https://iosLongUrl',
|
||||
|
||||
@@ -57,7 +57,7 @@ class UrlShortenerTest extends TestCase
|
||||
{
|
||||
$longUrl = 'http://foobar.com/12345/hello?foo=bar';
|
||||
$meta = ShortUrlCreation::fromRawData(['longUrl' => $longUrl]);
|
||||
$this->titleResolutionHelper->expects($this->once())->method('processTitleAndValidateUrl')->with(
|
||||
$this->titleResolutionHelper->expects($this->once())->method('processTitle')->with(
|
||||
$meta,
|
||||
)->willReturnArgument(0);
|
||||
$this->shortCodeHelper->method('ensureShortCodeUniqueness')->willReturn(true);
|
||||
@@ -90,7 +90,7 @@ class UrlShortenerTest extends TestCase
|
||||
);
|
||||
|
||||
$this->shortCodeHelper->expects($this->once())->method('ensureShortCodeUniqueness')->willReturn(false);
|
||||
$this->titleResolutionHelper->expects($this->once())->method('processTitleAndValidateUrl')->with(
|
||||
$this->titleResolutionHelper->expects($this->once())->method('processTitle')->with(
|
||||
$meta,
|
||||
)->willReturnArgument(0);
|
||||
|
||||
@@ -105,7 +105,7 @@ class UrlShortenerTest extends TestCase
|
||||
$repo = $this->createMock(ShortUrlRepository::class);
|
||||
$repo->expects($this->once())->method('findOneMatching')->willReturn($expected);
|
||||
$this->em->expects($this->once())->method('getRepository')->with(ShortUrl::class)->willReturn($repo);
|
||||
$this->titleResolutionHelper->expects($this->never())->method('processTitleAndValidateUrl');
|
||||
$this->titleResolutionHelper->expects($this->never())->method('processTitle');
|
||||
$this->shortCodeHelper->method('ensureShortCodeUniqueness')->willReturn(true);
|
||||
|
||||
$result = $this->urlShortener->shorten($meta);
|
||||
|
||||
Reference in New Issue
Block a user