Remove device long URLs support

This commit is contained in:
Alejandro Celaya
2024-02-27 18:46:49 +01:00
parent 4ad3dc0bc7
commit 36749658da
35 changed files with 118 additions and 747 deletions

View File

@@ -1,41 +0,0 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Core;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping\Builder\ClassMetadataBuilder;
use Doctrine\ORM\Mapping\Builder\FieldBuilder;
use Doctrine\ORM\Mapping\ClassMetadata;
use Shlinkio\Shlink\Core\Model\DeviceType;
return static function (ClassMetadata $metadata, array $emConfig): void {
$builder = new ClassMetadataBuilder($metadata);
$builder->setTable(determineTableName('device_long_urls', $emConfig));
$builder->createField('id', Types::BIGINT)
->columnName('id')
->makePrimaryKey()
->generatedValue('IDENTITY')
->option('unsigned', true)
->build();
(new FieldBuilder($builder, [
'fieldName' => 'deviceType',
'type' => Types::STRING,
'enumType' => DeviceType::class,
]))->columnName('device_type')
->length(255)
->build();
fieldWithUtf8Charset($builder->createField('longUrl', Types::TEXT), $emConfig)
->columnName('long_url')
->length(2048)
->build();
$builder->createManyToOne('shortUrl', ShortUrl\Entity\ShortUrl::class)
->addJoinColumn('short_url_id', 'id', nullable: false, onDelete: 'CASCADE')
->build();
};

View File

@@ -67,13 +67,6 @@ return static function (ClassMetadata $metadata, array $emConfig): void {
->fetchExtraLazy()
->build();
$builder->createOneToMany('deviceLongUrls', ShortUrl\Entity\DeviceLongUrl::class)
->mappedBy('shortUrl')
->cascadePersist()
->orphanRemoval()
->setIndexBy('deviceType')
->build();
$builder->createManyToMany('tags', Tag\Entity\Tag::class)
->setJoinTable(determineTableName('short_urls_in_tags', $emConfig))
->addInverseJoinColumn('tag_id', 'id', onDelete: 'CASCADE')

View File

@@ -4,6 +4,7 @@ namespace Shlinkio\Shlink\Core\RedirectRule\Entity;
use Psr\Http\Message\ServerRequestInterface;
use Shlinkio\Shlink\Common\Entity\AbstractEntity;
use Shlinkio\Shlink\Core\Model\DeviceType;
use Shlinkio\Shlink\Core\RedirectRule\Model\RedirectConditionType;
use function Shlinkio\Shlink\Core\acceptLanguageToLocales;
@@ -11,6 +12,7 @@ use function Shlinkio\Shlink\Core\ArrayUtils\some;
use function Shlinkio\Shlink\Core\normalizeLocale;
use function Shlinkio\Shlink\Core\splitLocale;
use function sprintf;
use function strtolower;
use function trim;
class RedirectCondition extends AbstractEntity
@@ -39,6 +41,14 @@ class RedirectCondition extends AbstractEntity
return new self($name, $type, $language);
}
public static function forDevice(DeviceType $device): self
{
$type = RedirectConditionType::DEVICE;
$name = sprintf('%s-%s', $type->value, $device->value);
return new self($name, $type, $device->value);
}
/**
* Tells if this condition matches provided request
*/
@@ -47,6 +57,7 @@ class RedirectCondition extends AbstractEntity
return match ($this->type) {
RedirectConditionType::QUERY_PARAM => $this->matchesQueryParam($request),
RedirectConditionType::LANGUAGE => $this->matchesLanguage($request),
RedirectConditionType::DEVICE => $this->matchesDevice($request),
};
}
@@ -81,4 +92,10 @@ class RedirectCondition extends AbstractEntity
},
);
}
private function matchesDevice(ServerRequestInterface $request): bool
{
$device = DeviceType::matchFromUserAgent($request->getHeaderLine('User-Agent'));
return $device !== null && $device->value === strtolower($this->matchValue);
}
}

View File

@@ -4,7 +4,6 @@ namespace Shlinkio\Shlink\Core\RedirectRule;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Http\Message\ServerRequestInterface;
use Shlinkio\Shlink\Core\Model\DeviceType;
use Shlinkio\Shlink\Core\RedirectRule\Entity\ShortUrlRedirectRule;
use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
@@ -27,7 +26,6 @@ readonly class ShortUrlRedirectionResolver implements ShortUrlRedirectionResolve
}
}
$device = DeviceType::matchFromUserAgent($request->getHeaderLine('User-Agent'));
return $shortUrl->longUrlForDevice($device);
return $shortUrl->getLongUrl();
}
}

View File

@@ -1,34 +0,0 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Core\ShortUrl\Entity;
use Shlinkio\Shlink\Common\Entity\AbstractEntity;
use Shlinkio\Shlink\Core\Model\DeviceType;
use Shlinkio\Shlink\Core\ShortUrl\Model\DeviceLongUrlPair;
class DeviceLongUrl extends AbstractEntity
{
private function __construct(
private readonly ShortUrl $shortUrl, // No need to read this field. It's used by doctrine
public readonly DeviceType $deviceType,
private string $longUrl,
) {
}
public static function fromShortUrlAndPair(ShortUrl $shortUrl, DeviceLongUrlPair $pair): self
{
return new self($shortUrl, $pair->deviceType, $pair->longUrl);
}
public function longUrl(): string
{
return $this->longUrl;
}
public function updateLongUrl(string $longUrl): void
{
$this->longUrl = $longUrl;
}
}

View File

@@ -12,8 +12,6 @@ use Doctrine\Common\Collections\Selectable;
use Shlinkio\Shlink\Common\Entity\AbstractEntity;
use Shlinkio\Shlink\Core\Domain\Entity\Domain;
use Shlinkio\Shlink\Core\Exception\ShortCodeCannotBeRegeneratedException;
use Shlinkio\Shlink\Core\Model\DeviceType;
use Shlinkio\Shlink\Core\ShortUrl\Model\DeviceLongUrlPair;
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlCreation;
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlEdition;
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlMode;
@@ -26,10 +24,7 @@ use Shlinkio\Shlink\Core\Visit\Model\VisitType;
use Shlinkio\Shlink\Importer\Model\ImportedShlinkUrl;
use Shlinkio\Shlink\Rest\Entity\ApiKey;
use function array_fill_keys;
use function array_map;
use function count;
use function Shlinkio\Shlink\Core\enumValues;
use function Shlinkio\Shlink\Core\generateRandomShortCode;
use function Shlinkio\Shlink\Core\normalizeDate;
use function Shlinkio\Shlink\Core\normalizeOptionalDate;
@@ -42,8 +37,6 @@ class ShortUrl extends AbstractEntity
private Chronos $dateCreated;
/** @var Collection<int, Visit> & Selectable */
private Collection & Selectable $visits;
/** @var Collection<string, DeviceLongUrl> */
private Collection $deviceLongUrls;
/** @var Collection<int, Tag> */
private Collection $tags;
private ?Chronos $validSince = null;
@@ -91,10 +84,6 @@ class ShortUrl extends AbstractEntity
$instance->longUrl = $creation->getLongUrl();
$instance->dateCreated = Chronos::now();
$instance->visits = new ArrayCollection();
$instance->deviceLongUrls = new ArrayCollection(array_map(
fn (DeviceLongUrlPair $pair) => DeviceLongUrl::fromShortUrlAndPair($instance, $pair),
$creation->deviceLongUrls,
));
$instance->tags = $relationResolver->resolveTags($creation->tags);
$instance->validSince = $creation->validSince;
$instance->validUntil = $creation->validUntil;
@@ -177,21 +166,6 @@ class ShortUrl extends AbstractEntity
if ($shortUrlEdit->forwardQueryWasProvided()) {
$this->forwardQuery = $shortUrlEdit->forwardQuery;
}
// Update device long URLs, removing, editing or creating where appropriate
foreach ($shortUrlEdit->devicesToRemove as $deviceType) {
$this->deviceLongUrls->remove($deviceType->value);
}
foreach ($shortUrlEdit->deviceLongUrls as $deviceLongUrlPair) {
$key = $deviceLongUrlPair->deviceType->value;
$deviceLongUrl = $this->deviceLongUrls->get($key);
if ($deviceLongUrl !== null) {
$deviceLongUrl->updateLongUrl($deviceLongUrlPair->longUrl);
} else {
$this->deviceLongUrls->set($key, DeviceLongUrl::fromShortUrlAndPair($this, $deviceLongUrlPair));
}
}
}
public function getLongUrl(): string
@@ -199,12 +173,6 @@ class ShortUrl extends AbstractEntity
return $this->longUrl;
}
public function longUrlForDevice(?DeviceType $deviceType): string
{
$deviceLongUrl = $deviceType === null ? null : $this->deviceLongUrls->get($deviceType->value);
return $deviceLongUrl?->longUrl() ?? $this->longUrl;
}
public function getShortCode(): string
{
return $this->shortCode;
@@ -332,14 +300,4 @@ class ShortUrl extends AbstractEntity
return true;
}
public function deviceLongUrls(): array
{
$data = array_fill_keys(enumValues(DeviceType::class), null);
foreach ($this->deviceLongUrls as $deviceUrl) {
$data[$deviceUrl->deviceType->value] = $deviceUrl->longUrl();
}
return $data;
}
}

View File

@@ -1,45 +0,0 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Core\ShortUrl\Model;
use Shlinkio\Shlink\Core\Model\DeviceType;
use function trim;
final class DeviceLongUrlPair
{
private function __construct(public readonly DeviceType $deviceType, public readonly string $longUrl)
{
}
public static function fromRawTypeAndLongUrl(string $type, string $longUrl): self
{
return new self(DeviceType::from($type), trim($longUrl));
}
/**
* Returns an array with two values.
* * The first one is a list of mapped instances for those entries in the map with non-null value
* * The second is a list of DeviceTypes which have been provided with value null
*
* @param array<string, string|null> $map
* @return array{array<string, self>, DeviceType[]}
*/
public static function fromMapToChangeSet(array $map): array
{
$pairsToKeep = [];
$deviceTypesToRemove = [];
foreach ($map as $deviceType => $longUrl) {
if ($longUrl === null) {
$deviceTypesToRemove[] = DeviceType::from($deviceType);
} else {
$pairsToKeep[$deviceType] = self::fromRawTypeAndLongUrl($deviceType, $longUrl);
}
}
return [$pairsToKeep, $deviceTypesToRemove];
}
}

View File

@@ -22,12 +22,10 @@ final readonly class ShortUrlCreation implements TitleResolutionModelInterface
{
/**
* @param string[] $tags
* @param DeviceLongUrlPair[] $deviceLongUrls
*/
private function __construct(
public string $longUrl,
public ShortUrlMode $shortUrlMode,
public array $deviceLongUrls = [],
public ?Chronos $validSince = null,
public ?Chronos $validUntil = null,
public ?string $customSlug = null,
@@ -55,14 +53,9 @@ final readonly class ShortUrlCreation implements TitleResolutionModelInterface
throw ValidationException::fromInputFilter($inputFilter);
}
[$deviceLongUrls] = DeviceLongUrlPair::fromMapToChangeSet(
$inputFilter->getValue(ShortUrlInputFilter::DEVICE_LONG_URLS) ?? [],
);
return new self(
longUrl: $inputFilter->getValue(ShortUrlInputFilter::LONG_URL),
shortUrlMode: $options->mode,
deviceLongUrls: $deviceLongUrls,
validSince: normalizeOptionalDate($inputFilter->getValue(ShortUrlInputFilter::VALID_SINCE)),
validUntil: normalizeOptionalDate($inputFilter->getValue(ShortUrlInputFilter::VALID_UNTIL)),
customSlug: $inputFilter->getValue(ShortUrlInputFilter::CUSTOM_SLUG),
@@ -87,7 +80,6 @@ final readonly class ShortUrlCreation implements TitleResolutionModelInterface
return new self(
longUrl: $this->longUrl,
shortUrlMode: $this->shortUrlMode,
deviceLongUrls: $this->deviceLongUrls,
validSince: $this->validSince,
validUntil: $this->validUntil,
customSlug: $this->customSlug,

View File

@@ -6,7 +6,6 @@ namespace Shlinkio\Shlink\Core\ShortUrl\Model;
use Cake\Chronos\Chronos;
use Shlinkio\Shlink\Core\Exception\ValidationException;
use Shlinkio\Shlink\Core\Model\DeviceType;
use Shlinkio\Shlink\Core\ShortUrl\Helper\TitleResolutionModelInterface;
use Shlinkio\Shlink\Core\ShortUrl\Model\Validation\ShortUrlInputFilter;
@@ -19,14 +18,10 @@ final readonly class ShortUrlEdition implements TitleResolutionModelInterface
{
/**
* @param string[] $tags
* @param DeviceLongUrlPair[] $deviceLongUrls
* @param DeviceType[] $devicesToRemove
*/
private function __construct(
private bool $longUrlPropWasProvided = false,
public ?string $longUrl = null,
public array $deviceLongUrls = [],
public array $devicesToRemove = [],
private bool $validSincePropWasProvided = false,
public ?Chronos $validSince = null,
private bool $validUntilPropWasProvided = false,
@@ -55,15 +50,9 @@ final readonly class ShortUrlEdition implements TitleResolutionModelInterface
throw ValidationException::fromInputFilter($inputFilter);
}
[$deviceLongUrls, $devicesToRemove] = DeviceLongUrlPair::fromMapToChangeSet(
$inputFilter->getValue(ShortUrlInputFilter::DEVICE_LONG_URLS) ?? [],
);
return new self(
longUrlPropWasProvided: array_key_exists(ShortUrlInputFilter::LONG_URL, $data),
longUrl: $inputFilter->getValue(ShortUrlInputFilter::LONG_URL),
deviceLongUrls: $deviceLongUrls,
devicesToRemove: $devicesToRemove,
validSincePropWasProvided: array_key_exists(ShortUrlInputFilter::VALID_SINCE, $data),
validSince: normalizeOptionalDate($inputFilter->getValue(ShortUrlInputFilter::VALID_SINCE)),
validUntilPropWasProvided: array_key_exists(ShortUrlInputFilter::VALID_UNTIL, $data),
@@ -86,8 +75,6 @@ final readonly class ShortUrlEdition implements TitleResolutionModelInterface
return new self(
longUrlPropWasProvided: $this->longUrlPropWasProvided,
longUrl: $this->longUrl,
deviceLongUrls: $this->deviceLongUrls,
devicesToRemove: $this->devicesToRemove,
validSincePropWasProvided: $this->validSincePropWasProvided,
validSince: $this->validSince,
validUntilPropWasProvided: $this->validUntilPropWasProvided,

View File

@@ -1,57 +0,0 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Core\ShortUrl\Model\Validation;
use Laminas\Validator\AbstractValidator;
use Laminas\Validator\ValidatorInterface;
use Shlinkio\Shlink\Core\Model\DeviceType;
use function array_keys;
use function array_values;
use function is_array;
use function Shlinkio\Shlink\Core\ArrayUtils\contains;
use function Shlinkio\Shlink\Core\ArrayUtils\every;
use function Shlinkio\Shlink\Core\enumValues;
class DeviceLongUrlsValidator extends AbstractValidator
{
private const NOT_ARRAY = 'NOT_ARRAY';
private const INVALID_DEVICE = 'INVALID_DEVICE';
private const INVALID_LONG_URL = 'INVALID_LONG_URL';
protected array $messageTemplates = [
self::NOT_ARRAY => 'Provided value is not an array.',
self::INVALID_DEVICE => 'You have provided at least one invalid device identifier.',
self::INVALID_LONG_URL => 'At least one of the long URLs are invalid.',
];
public function __construct(private readonly ValidatorInterface $longUrlValidators)
{
parent::__construct();
}
public function isValid(mixed $value): bool
{
if (! is_array($value)) {
$this->error(self::NOT_ARRAY);
return false;
}
$validValues = enumValues(DeviceType::class);
$keys = array_keys($value);
if (! every($keys, static fn ($key) => contains($key, $validValues))) {
$this->error(self::INVALID_DEVICE);
return false;
}
$longUrls = array_values($value);
$result = every($longUrls, $this->longUrlValidators->isValid(...));
if (! $result) {
$this->error(self::INVALID_LONG_URL);
}
return $result;
}
}

View File

@@ -31,7 +31,6 @@ class ShortUrlInputFilter extends InputFilter
// Fields for creation and edition
public const LONG_URL = 'longUrl';
public const DEVICE_LONG_URLS = 'deviceLongUrls';
public const VALID_SINCE = 'validSince';
public const VALID_UNTIL = 'validUntil';
public const MAX_VISITS = 'maxVisits';
@@ -97,12 +96,6 @@ class ShortUrlInputFilter extends InputFilter
$longUrlInput->getValidatorChain()->merge($this->longUrlValidators());
$this->add($longUrlInput);
$deviceLongUrlsInput = InputFactory::basic(self::DEVICE_LONG_URLS);
$deviceLongUrlsInput->getValidatorChain()->attach(
new DeviceLongUrlsValidator($this->longUrlValidators(allowNull: ! $requireLongUrl)),
);
$this->add($deviceLongUrlsInput);
$validSince = InputFactory::basic(self::VALID_SINCE);
$validSince->getValidatorChain()->attach(new Validator\Date(['format' => DateTimeInterface::ATOM]));
$this->add($validSince);

View File

@@ -27,7 +27,6 @@ class ShortUrlDataTransformer implements DataTransformerInterface
'shortCode' => $shortUrl->getShortCode(),
'shortUrl' => $this->stringifier->stringify($shortUrl),
'longUrl' => $shortUrl->getLongUrl(),
'deviceLongUrls' => $shortUrl->deviceLongUrls(),
'dateCreated' => $shortUrl->getDateCreated()->toAtomString(),
'tags' => array_map(static fn (Tag $tag) => $tag->__toString(), $shortUrl->getTags()->toArray()),
'meta' => $this->buildMeta($shortUrl),

View File

@@ -51,7 +51,6 @@ class PublishingUpdatesGeneratorTest extends TestCase
'shortCode' => $shortUrl->getShortCode(),
'shortUrl' => 'http:/' . $shortUrl->getShortCode(),
'longUrl' => 'https://longUrl',
'deviceLongUrls' => $shortUrl->deviceLongUrls(),
'dateCreated' => $shortUrl->getDateCreated()->toAtomString(),
'tags' => [],
'meta' => [
@@ -125,7 +124,6 @@ class PublishingUpdatesGeneratorTest extends TestCase
'shortCode' => $shortUrl->getShortCode(),
'shortUrl' => 'http:/' . $shortUrl->getShortCode(),
'longUrl' => 'https://longUrl',
'deviceLongUrls' => $shortUrl->deviceLongUrls(),
'dateCreated' => $shortUrl->getDateCreated()->toAtomString(),
'tags' => [],
'meta' => [

View File

@@ -1,6 +1,6 @@
<?php
namespace RedirectRule\Entity;
namespace ShlinkioTest\Shlink\Core\RedirectRule\Entity;
use Laminas\Diactoros\ServerRequestFactory;
use PHPUnit\Framework\Attributes\DataProvider;

View File

@@ -1,6 +1,6 @@
<?php
namespace RedirectRule;
namespace ShlinkioTest\Shlink\Core\RedirectRule;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\EntityManagerInterface;
@@ -41,10 +41,6 @@ class ShortUrlRedirectionResolverTest extends TestCase
): 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',
],
]));
$repo = $this->createMock(EntityRepository::class);
@@ -75,12 +71,16 @@ class ShortUrlRedirectionResolverTest extends TestCase
'https://example.com/foo/bar',
];
yield 'desktop user agent' => [$request(DESKTOP_USER_AGENT), null, 'https://example.com/foo/bar'];
yield 'android user agent' => [
yield 'matching android device' => [
$request(ANDROID_USER_AGENT),
RedirectCondition::forQueryParam('foo', 'bar'), // This condition won't match
'https://example.com/android',
RedirectCondition::forDevice(DeviceType::ANDROID),
'https://example.com/from-rule',
];
yield 'matching ios device' => [
$request(IOS_USER_AGENT),
RedirectCondition::forDevice(DeviceType::IOS),
'https://example.com/from-rule',
];
yield 'ios user agent' => [$request(IOS_USER_AGENT), null, 'https://example.com/ios'];
yield 'matching language' => [
$request()->withHeader('Accept-Language', 'es-ES'),
RedirectCondition::forLanguage('es-ES'),

View File

@@ -10,11 +10,9 @@ use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\Attributes\TestWith;
use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\Core\Exception\ShortCodeCannotBeRegeneratedException;
use Shlinkio\Shlink\Core\Model\DeviceType;
use Shlinkio\Shlink\Core\Options\UrlShortenerOptions;
use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlCreation;
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlEdition;
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlMode;
use Shlinkio\Shlink\Core\ShortUrl\Model\Validation\ShortUrlInputFilter;
use Shlinkio\Shlink\Importer\Model\ImportedShlinkUrl;
@@ -113,48 +111,6 @@ class ShortUrlTest extends TestCase
self::assertEquals($expectedShortCodeLength, strlen($shortCode));
}
#[Test]
public function deviceLongUrlsAreUpdated(): void
{
$shortUrl = ShortUrl::withLongUrl('https://foo');
$shortUrl->update(ShortUrlEdition::fromRawData([
ShortUrlInputFilter::DEVICE_LONG_URLS => [
DeviceType::ANDROID->value => 'https://android',
DeviceType::IOS->value => 'https://ios',
],
]));
self::assertEquals([
DeviceType::ANDROID->value => 'https://android',
DeviceType::IOS->value => 'https://ios',
DeviceType::DESKTOP->value => null,
], $shortUrl->deviceLongUrls());
$shortUrl->update(ShortUrlEdition::fromRawData([
ShortUrlInputFilter::DEVICE_LONG_URLS => [
DeviceType::ANDROID->value => null,
DeviceType::DESKTOP->value => 'https://desktop',
],
]));
self::assertEquals([
DeviceType::ANDROID->value => null,
DeviceType::IOS->value => 'https://ios',
DeviceType::DESKTOP->value => 'https://desktop',
], $shortUrl->deviceLongUrls());
$shortUrl->update(ShortUrlEdition::fromRawData([
ShortUrlInputFilter::DEVICE_LONG_URLS => [
DeviceType::ANDROID->value => null,
DeviceType::IOS->value => null,
],
]));
self::assertEquals([
DeviceType::ANDROID->value => null,
DeviceType::IOS->value => null,
DeviceType::DESKTOP->value => 'https://desktop',
], $shortUrl->deviceLongUrls());
}
#[Test]
public function generatesLowercaseOnlyShortCodesInLooseMode(): void
{

View File

@@ -9,7 +9,6 @@ use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\Core\Exception\ValidationException;
use Shlinkio\Shlink\Core\Model\DeviceType;
use Shlinkio\Shlink\Core\Options\UrlShortenerOptions;
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlCreation;
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlMode;
@@ -79,43 +78,6 @@ class ShortUrlCreationTest extends TestCase
yield [[
ShortUrlInputFilter::LONG_URL => 'missing_schema',
]];
yield [[
ShortUrlInputFilter::LONG_URL => 'https://foo',
ShortUrlInputFilter::DEVICE_LONG_URLS => [
'invalid' => 'https://shlink.io',
],
]];
yield [[
ShortUrlInputFilter::LONG_URL => 'https://foo',
ShortUrlInputFilter::DEVICE_LONG_URLS => [
DeviceType::DESKTOP->value => '',
],
]];
yield [[
ShortUrlInputFilter::LONG_URL => 'https://foo',
ShortUrlInputFilter::DEVICE_LONG_URLS => [
DeviceType::DESKTOP->value => null,
],
]];
yield [[
ShortUrlInputFilter::LONG_URL => 'https://foo',
ShortUrlInputFilter::DEVICE_LONG_URLS => [
DeviceType::IOS->value => ' ',
],
]];
yield [[
ShortUrlInputFilter::LONG_URL => 'https://foo',
ShortUrlInputFilter::DEVICE_LONG_URLS => [
DeviceType::ANDROID->value => 'missing_schema',
],
]];
yield [[
ShortUrlInputFilter::LONG_URL => 'https://foo',
ShortUrlInputFilter::DEVICE_LONG_URLS => [
DeviceType::IOS->value => 'https://bar',
DeviceType::ANDROID->value => [],
],
]];
}
#[Test, DataProvider('provideCustomSlugs')]

View File

@@ -1,59 +0,0 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\Core\ShortUrl\Model;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\Core\Model\DeviceType;
use Shlinkio\Shlink\Core\ShortUrl\Model\DeviceLongUrlPair;
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlEdition;
use Shlinkio\Shlink\Core\ShortUrl\Model\Validation\ShortUrlInputFilter;
class ShortUrlEditionTest extends TestCase
{
#[Test, DataProvider('provideDeviceLongUrls')]
public function expectedDeviceLongUrlsAreResolved(
?array $deviceLongUrls,
array $expectedDeviceLongUrls,
array $expectedDevicesToRemove,
): void {
$edition = ShortUrlEdition::fromRawData([ShortUrlInputFilter::DEVICE_LONG_URLS => $deviceLongUrls]);
self::assertEquals($expectedDeviceLongUrls, $edition->deviceLongUrls);
self::assertEquals($expectedDevicesToRemove, $edition->devicesToRemove);
}
public static function provideDeviceLongUrls(): iterable
{
yield 'null' => [null, [], []];
yield 'empty' => [[], [], []];
yield 'only new urls' => [[
DeviceType::DESKTOP->value => 'https://foo',
DeviceType::IOS->value => 'https://bar',
], [
DeviceType::DESKTOP->value => DeviceLongUrlPair::fromRawTypeAndLongUrl(
DeviceType::DESKTOP->value,
'https://foo',
),
DeviceType::IOS->value => DeviceLongUrlPair::fromRawTypeAndLongUrl(DeviceType::IOS->value, 'https://bar'),
], []];
yield 'only urls to remove' => [[
DeviceType::ANDROID->value => null,
DeviceType::IOS->value => null,
], [], [DeviceType::ANDROID, DeviceType::IOS]];
yield 'both' => [[
DeviceType::DESKTOP->value => 'https://bar',
DeviceType::IOS->value => 'https://foo',
DeviceType::ANDROID->value => null,
], [
DeviceType::DESKTOP->value => DeviceLongUrlPair::fromRawTypeAndLongUrl(
DeviceType::DESKTOP->value,
'https://bar',
),
DeviceType::IOS->value => DeviceLongUrlPair::fromRawTypeAndLongUrl(DeviceType::IOS->value, 'https://foo'),
], [DeviceType::ANDROID]];
}
}

View File

@@ -1,70 +0,0 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\Core\ShortUrl\Model\Validation;
use Laminas\Validator\NotEmpty;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\Core\Model\DeviceType;
use Shlinkio\Shlink\Core\ShortUrl\Model\Validation\DeviceLongUrlsValidator;
use stdClass;
class DeviceLongUrlsValidatorTest extends TestCase
{
private DeviceLongUrlsValidator $validator;
protected function setUp(): void
{
$this->validator = new DeviceLongUrlsValidator(new NotEmpty());
}
#[Test, DataProvider('provideNonArrayValues')]
public function nonArrayValuesAreNotValid(mixed $invalidValue): void
{
self::assertFalse($this->validator->isValid($invalidValue));
self::assertEquals(['NOT_ARRAY' => 'Provided value is not an array.'], $this->validator->getMessages());
}
public static function provideNonArrayValues(): iterable
{
yield 'int' => [0];
yield 'float' => [100.45];
yield 'string' => ['foo'];
yield 'boolean' => [true];
yield 'object' => [new stdClass()];
yield 'null' => [null];
}
#[Test]
public function unrecognizedKeysAreNotValid(): void
{
self::assertFalse($this->validator->isValid(['foo' => 'bar']));
self::assertEquals(
['INVALID_DEVICE' => 'You have provided at least one invalid device identifier.'],
$this->validator->getMessages(),
);
}
#[Test]
public function everyUrlMustMatchLongUrlValidator(): void
{
self::assertFalse($this->validator->isValid([DeviceType::ANDROID->value => '']));
self::assertEquals(
['INVALID_LONG_URL' => 'At least one of the long URLs are invalid.'],
$this->validator->getMessages(),
);
}
#[Test]
public function validValuesResultInValidResult(): void
{
self::assertTrue($this->validator->isValid([
DeviceType::ANDROID->value => 'foo',
DeviceType::IOS->value => 'bar',
DeviceType::DESKTOP->value => 'baz',
]));
}
}

View File

@@ -12,7 +12,6 @@ use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\MockObject\Rule\InvocationOrder;
use PHPUnit\Framework\MockObject\Rule\InvokedCount;
use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\Core\Model\DeviceType;
use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlTitleResolutionHelperInterface;
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlEdition;
@@ -22,9 +21,6 @@ use Shlinkio\Shlink\Core\ShortUrl\ShortUrlResolverInterface;
use Shlinkio\Shlink\Core\ShortUrl\ShortUrlService;
use Shlinkio\Shlink\Rest\Entity\ApiKey;
use function array_fill_keys;
use function Shlinkio\Shlink\Core\enumValues;
class ShortUrlServiceTest extends TestCase
{
private ShortUrlService $service;
@@ -73,21 +69,11 @@ class ShortUrlServiceTest extends TestCase
$apiKey,
);
$resolveDeviceLongUrls = function () use ($shortUrlEdit): array {
$result = array_fill_keys(enumValues(DeviceType::class), null);
foreach ($shortUrlEdit->deviceLongUrls ?? [] as $longUrl) {
$result[$longUrl->deviceType->value] = $longUrl->longUrl;
}
return $result;
};
self::assertSame($shortUrl, $result);
self::assertEquals($shortUrlEdit->validSince, $shortUrl->getValidSince());
self::assertEquals($shortUrlEdit->validUntil, $shortUrl->getValidUntil());
self::assertEquals($shortUrlEdit->maxVisits, $shortUrl->getMaxVisits());
self::assertEquals($shortUrlEdit->longUrl ?? $originalLongUrl, $shortUrl->getLongUrl());
self::assertEquals($resolveDeviceLongUrls(), $shortUrl->deviceLongUrls());
}
public static function provideShortUrlEdits(): iterable
@@ -102,11 +88,5 @@ class ShortUrlServiceTest extends TestCase
'maxVisits' => 10,
'longUrl' => 'https://modifiedLongUrl',
]), ApiKey::create()];
yield 'device redirects' => [new InvokedCount(0), ShortUrlEdition::fromRawData([
'deviceLongUrls' => [
DeviceType::IOS->value => 'https://iosLongUrl',
DeviceType::ANDROID->value => 'https://androidLongUrl',
],
]), null];
}
}