Create component to resolve the long URL to redirect to for a short URL

This commit is contained in:
Alejandro Celaya 2024-02-24 23:10:08 +01:00
parent 68b77e22c5
commit 09e81b00c5
6 changed files with 121 additions and 31 deletions

View File

@ -32,6 +32,8 @@ return [
Options\QrCodeOptions::class => [ValinorConfigFactory::class, 'config.qr_codes'],
Options\RabbitMqOptions::class => [ValinorConfigFactory::class, 'config.rabbitmq'],
RedirectRule\ShortUrlRedirectionResolver::class => ConfigAbstractFactory::class,
ShortUrl\UrlShortener::class => ConfigAbstractFactory::class,
ShortUrl\ShortUrlService::class => ConfigAbstractFactory::class,
ShortUrl\ShortUrlListService::class => ConfigAbstractFactory::class,
@ -156,6 +158,7 @@ return [
Util\RedirectResponseHelper::class => [Options\RedirectOptions::class],
Config\NotFoundRedirectResolver::class => [Util\RedirectResponseHelper::class, 'Logger_Shlink'],
RedirectRule\ShortUrlRedirectionResolver::class => ['em'],
Action\RedirectAction::class => [
ShortUrl\ShortUrlResolver::class,
@ -179,7 +182,10 @@ return [
],
ShortUrl\Helper\ShortUrlStringifier::class => ['config.url_shortener.domain', 'config.router.base_path'],
ShortUrl\Helper\ShortUrlTitleResolutionHelper::class => ['httpClient', Options\UrlShortenerOptions::class],
ShortUrl\Helper\ShortUrlRedirectionBuilder::class => [Options\TrackingOptions::class],
ShortUrl\Helper\ShortUrlRedirectionBuilder::class => [
Options\TrackingOptions::class,
RedirectRule\ShortUrlRedirectionResolver::class,
],
ShortUrl\Transformer\ShortUrlDataTransformer::class => [ShortUrl\Helper\ShortUrlStringifier::class],
ShortUrl\Middleware\ExtraPathRedirectMiddleware::class => [
ShortUrl\ShortUrlResolver::class,

View File

@ -0,0 +1,23 @@
<?php
namespace Shlinkio\Shlink\Core\RedirectRule;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Http\Message\ServerRequestInterface;
use Shlinkio\Shlink\Core\Model\DeviceType;
use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
readonly class ShortUrlRedirectionResolver implements ShortUrlRedirectionResolverInterface
{
public function __construct(private EntityManagerInterface $em)
{
}
public function resolveLongUrl(ShortUrl $shortUrl, ServerRequestInterface $request): string
{
// TODO Resolve rules and check if any of them matches
$device = DeviceType::matchFromUserAgent($request->getHeaderLine('User-Agent'));
return $shortUrl->longUrlForDevice($device);
}
}

View File

@ -0,0 +1,11 @@
<?php
namespace Shlinkio\Shlink\Core\RedirectRule;
use Psr\Http\Message\ServerRequestInterface;
use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
interface ShortUrlRedirectionResolverInterface
{
public function resolveLongUrl(ShortUrl $shortUrl, ServerRequestInterface $request): string;
}

View File

@ -8,16 +8,18 @@ use GuzzleHttp\Psr7\Query;
use Laminas\Diactoros\Uri;
use Laminas\Stdlib\ArrayUtils;
use Psr\Http\Message\ServerRequestInterface;
use Shlinkio\Shlink\Core\Model\DeviceType;
use Shlinkio\Shlink\Core\Options\TrackingOptions;
use Shlinkio\Shlink\Core\RedirectRule\ShortUrlRedirectionResolverInterface;
use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
use function sprintf;
class ShortUrlRedirectionBuilder implements ShortUrlRedirectionBuilderInterface
readonly class ShortUrlRedirectionBuilder implements ShortUrlRedirectionBuilderInterface
{
public function __construct(private readonly TrackingOptions $trackingOptions)
{
public function __construct(
private TrackingOptions $trackingOptions,
private ShortUrlRedirectionResolverInterface $redirectionResolver,
) {
}
public function buildShortUrlRedirect(
@ -25,9 +27,8 @@ class ShortUrlRedirectionBuilder implements ShortUrlRedirectionBuilderInterface
ServerRequestInterface $request,
?string $extraPath = null,
): string {
$uri = new Uri($this->redirectionResolver->resolveLongUrl($shortUrl, $request));
$currentQuery = $request->getQueryParams();
$device = DeviceType::matchFromUserAgent($request->getHeaderLine('User-Agent'));
$uri = new Uri($shortUrl->longUrlForDevice($device));
$shouldForwardQuery = $shortUrl->forwardQuery();
return $uri

View File

@ -0,0 +1,60 @@
<?php
namespace RedirectRule;
use Doctrine\ORM\EntityManagerInterface;
use Laminas\Diactoros\ServerRequestFactory;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Http\Message\ServerRequestInterface;
use Shlinkio\Shlink\Core\Model\DeviceType;
use Shlinkio\Shlink\Core\RedirectRule\ShortUrlRedirectionResolver;
use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlCreation;
use const ShlinkioTest\Shlink\ANDROID_USER_AGENT;
use const ShlinkioTest\Shlink\DESKTOP_USER_AGENT;
use const ShlinkioTest\Shlink\IOS_USER_AGENT;
class ShortUrlRedirectionResolverTest extends TestCase
{
private ShortUrlRedirectionResolver $resolver;
private EntityManagerInterface & MockObject $em;
protected function setUp(): void
{
$this->em = $this->createMock(EntityManagerInterface::class);
$this->resolver = new ShortUrlRedirectionResolver($this->em);
}
#[Test, DataProvider('provideData')]
public function resolveLongUrlReturnsExpectedValue(ServerRequestInterface $request, string $expectedUrl): void
{
$shortUrl = ShortUrl::create(ShortUrlCreation::fromRawData([
'longUrl' => 'https://example.com/foo/bar',
'deviceLongUrls' => [
DeviceType::ANDROID->value => 'https://example.com/android',
DeviceType::IOS->value => 'https://example.com/ios',
],
]));
$result = $this->resolver->resolveLongUrl($shortUrl, $request);
self::assertEquals($expectedUrl, $result);
}
public static function provideData(): iterable
{
$request = static fn (string $userAgent = '') => ServerRequestFactory::fromGlobals()->withHeader(
'User-Agent',
$userAgent,
);
yield 'unknown user agent' => [$request('Unknown'), 'https://example.com/foo/bar'];
yield 'desktop user agent' => [$request(DESKTOP_USER_AGENT), 'https://example.com/foo/bar'];
yield 'android user agent' => [$request(ANDROID_USER_AGENT), 'https://example.com/android'];
yield 'ios user agent' => [$request(IOS_USER_AGENT), 'https://example.com/ios'];
}
}

View File

@ -7,26 +7,26 @@ namespace ShlinkioTest\Shlink\Core\ShortUrl\Helper;
use Laminas\Diactoros\ServerRequestFactory;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Http\Message\ServerRequestInterface;
use Shlinkio\Shlink\Core\Model\DeviceType;
use Shlinkio\Shlink\Core\Options\TrackingOptions;
use Shlinkio\Shlink\Core\RedirectRule\ShortUrlRedirectionResolverInterface;
use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlRedirectionBuilder;
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlCreation;
use const ShlinkioTest\Shlink\ANDROID_USER_AGENT;
use const ShlinkioTest\Shlink\DESKTOP_USER_AGENT;
use const ShlinkioTest\Shlink\IOS_USER_AGENT;
class ShortUrlRedirectionBuilderTest extends TestCase
{
private ShortUrlRedirectionBuilder $redirectionBuilder;
private ShortUrlRedirectionResolverInterface & MockObject $redirectionResolver;
protected function setUp(): void
{
$trackingOptions = new TrackingOptions(disableTrackParam: 'foobar');
$this->redirectionBuilder = new ShortUrlRedirectionBuilder($trackingOptions);
$this->redirectionResolver = $this->createMock(ShortUrlRedirectionResolverInterface::class);
$this->redirectionBuilder = new ShortUrlRedirectionBuilder($trackingOptions, $this->redirectionResolver);
}
#[Test, DataProvider('provideData')]
@ -39,11 +39,12 @@ class ShortUrlRedirectionBuilderTest extends TestCase
$shortUrl = ShortUrl::create(ShortUrlCreation::fromRawData([
'longUrl' => 'https://domain.com/foo/bar?some=thing',
'forwardQuery' => $forwardQuery,
'deviceLongUrls' => [
DeviceType::ANDROID->value => 'https://domain.com/android',
DeviceType::IOS->value => 'https://domain.com/ios',
],
]));
$this->redirectionResolver->expects($this->once())->method('resolveLongUrl')->with(
$shortUrl,
$request,
)->willReturn($shortUrl->getLongUrl());
$result = $this->redirectionBuilder->buildShortUrlRedirect($shortUrl, $request, $extraPath);
self::assertEquals($expectedUrl, $result);
@ -72,7 +73,7 @@ class ShortUrlRedirectionBuilderTest extends TestCase
];
yield [
'https://domain.com/foo/bar?some=overwritten',
$request(['foobar' => 'notrack', 'some' => 'overwritten'])->withHeader('User-Agent', 'Unknown'),
$request(['foobar' => 'notrack', 'some' => 'overwritten']),
null,
true,
];
@ -91,7 +92,7 @@ class ShortUrlRedirectionBuilderTest extends TestCase
yield ['https://domain.com/foo/bar/something/else-baz?some=thing', $request(), '/something/else-baz', true];
yield [
'https://domain.com/foo/bar/something/else-baz?some=thing&hello=world',
$request(['hello' => 'world'])->withHeader('User-Agent', DESKTOP_USER_AGENT),
$request(['hello' => 'world']),
'/something/else-baz',
true,
];
@ -107,17 +108,5 @@ class ShortUrlRedirectionBuilderTest extends TestCase
'/something/else-baz',
false,
];
yield [
'https://domain.com/android/something',
$request(['foo' => 'bar'])->withHeader('User-Agent', ANDROID_USER_AGENT),
'/something',
false,
];
yield [
'https://domain.com/ios?foo=bar',
$request(['foo' => 'bar'])->withHeader('User-Agent', IOS_USER_AGENT),
null,
null,
];
}
}