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

@ -16,6 +16,9 @@
If you want to keep previous behavior, pass `QR_CODE_FOR_DISABLED_SHORT_URLS=false` or the equivalent configuration option.
* Long URL title resolution is now enabled by default. You can still disable it by passing `AUTO_RESOLVE_TITLES=false` or the equivalent configuration option.
* Shlink no longer allows to opt-in for long URL verification. Long URLs are unconditionally considered correct during short URL creation/edition.
* Device long URLs have been migrated to the new Dynamic rule-based redirects system.
All existing short URLs which were using device long URLs will be automatically migrated and continue working as expected, but the API surface has changed.
If you use shlink-web-client and rely on this feature when creating/updating short URLs, **DO NOT UPDATE YET**. Support for dynamic rule-based redirects will be added to shlink-web-client soon, in v4.1.0
### Changes in REST API
@ -34,6 +37,7 @@
* Endpoints previously returning props like `"visitsCount": {number}` no longer do it. There should be an alternative `"visitsSummary": {}` object with the amount nested on it.
* It is no longer possible to order the short URLs list with `orderBy=visitsCount-ASC`/`orderBy=visitsCount-DESC`. Use `orderBy=visits-ASC`/`orderBy=visits-DESC` instead.
* It is no longer possible to get tags with stats using `GET /tags?withStats=true`. Use `GET /tags/stats` endpoint instead.
* The `deviceLongUrls` are ignored when calling `POST /short-urls` or `PATCH /short-urls/{shortCode}`. These should now be configured as dynamic rule-based redirects via `POST /short-urls/{shortCode}/redirect-rules`.
### Changes in Docker image

View File

@ -111,9 +111,6 @@
"type": "string",
"description": "The original long URL."
},
"deviceLongUrls": {
"$ref": "#/components/schemas/DeviceLongUrls"
},
"dateCreated": {
"type": "string",
"format": "date-time",
@ -150,11 +147,6 @@
"shortCode": "12C18",
"shortUrl": "https://s.test/12C18",
"longUrl": "https://store.steampowered.com",
"deviceLongUrls": {
"android": "https://store.steampowered.com/android",
"ios": "https://store.steampowered.com/ios",
"desktop": null
},
"dateCreated": "2016-08-21T20:34:16+02:00",
"visitsSummary": {
"total": 328,
@ -218,24 +210,6 @@
}
}
},
"DeviceLongUrls": {
"type": "object",
"required": ["android", "ios", "desktop"],
"properties": {
"android": {
"description": "The long URL to redirect to when the short URL is visited from a device running Android",
"type": "string"
},
"ios": {
"description": "The long URL to redirect to when the short URL is visited from a device running iOS",
"type": "string"
},
"desktop": {
"description": "The long URL to redirect to when the short URL is visited from a desktop browser",
"type": "string"
}
}
},
"Visit": {
"type": "object",
"properties": {

View File

@ -1,17 +0,0 @@
{
"type": "object",
"properties": {
"android": {
"description": "The long URL to redirect to when the short URL is visited from a device running Android",
"type": ["string"]
},
"ios": {
"description": "The long URL to redirect to when the short URL is visited from a device running iOS",
"type": ["string"]
},
"desktop": {
"description": "The long URL to redirect to when the short URL is visited from a desktop browser",
"type": ["string"]
}
}
}

View File

@ -1,17 +0,0 @@
{
"type": "object",
"allOf": [{
"$ref": "./DeviceLongUrls.json"
}],
"properties": {
"android": {
"type": ["null"]
},
"ios": {
"type": ["null"]
},
"desktop": {
"type": ["null"]
}
}
}

View File

@ -1,7 +0,0 @@
{
"type": "object",
"required": ["android", "ios", "desktop"],
"allOf": [{
"$ref": "./DeviceLongUrlsEdit.json"
}]
}

View File

@ -4,7 +4,6 @@
"shortCode",
"shortUrl",
"longUrl",
"deviceLongUrls",
"dateCreated",
"visitsSummary",
"tags",
@ -27,9 +26,6 @@
"type": "string",
"description": "The original long URL."
},
"deviceLongUrls": {
"$ref": "./DeviceLongUrlsResp.json"
},
"dateCreated": {
"type": "string",
"format": "date-time",

View File

@ -5,9 +5,6 @@
"description": "The long URL this short URL will redirect to",
"type": "string"
},
"deviceLongUrls": {
"$ref": "./DeviceLongUrlsEdit.json"
},
"validSince": {
"description": "The date (in ISO-8601 format) from which this short code will be valid",
"type": ["string", "null"]

View File

@ -163,11 +163,6 @@
"shortCode": "12C18",
"shortUrl": "https://s.test/12C18",
"longUrl": "https://store.steampowered.com",
"deviceLongUrls": {
"android": null,
"ios": null,
"desktop": null
},
"dateCreated": "2016-08-21T20:34:16+02:00",
"visitsSummary": {
"total": 328,
@ -191,11 +186,6 @@
"shortCode": "12Kb3",
"shortUrl": "https://s.test/12Kb3",
"longUrl": "https://shlink.io",
"deviceLongUrls": {
"android": null,
"ios": "https://shlink.io/ios",
"desktop": null
},
"dateCreated": "2016-05-01T20:34:16+02:00",
"visitsSummary": {
"total": 1029,
@ -218,11 +208,6 @@
"shortCode": "123bA",
"shortUrl": "https://example.com/123bA",
"longUrl": "https://www.google.com",
"deviceLongUrls": {
"android": null,
"ios": null,
"desktop": null
},
"dateCreated": "2015-10-01T20:34:16+02:00",
"visitsSummary": {
"total": 25,
@ -296,9 +281,6 @@
"type": "object",
"required": ["longUrl"],
"properties": {
"deviceLongUrls": {
"$ref": "../definitions/DeviceLongUrls.json"
},
"customSlug": {
"description": "A unique custom slug to be used instead of the generated short code",
"type": "string"
@ -338,11 +320,6 @@
"shortCode": "12C18",
"shortUrl": "https://s.test/12C18",
"longUrl": "https://store.steampowered.com",
"deviceLongUrls": {
"android": null,
"ios": null,
"desktop": null
},
"dateCreated": "2016-08-21T20:34:16+02:00",
"visitsSummary": {
"total": 0,

View File

@ -53,11 +53,6 @@
},
"example": {
"longUrl": "https://github.com/shlinkio/shlink",
"deviceLongUrls": {
"android": null,
"ios": null,
"desktop": null
},
"shortUrl": "https://s.test/abc123",
"shortCode": "abc123",
"dateCreated": "2016-08-21T20:34:16+02:00",

View File

@ -34,11 +34,6 @@
"shortCode": "12Kb3",
"shortUrl": "https://s.test/12Kb3",
"longUrl": "https://shlink.io",
"deviceLongUrls": {
"android": null,
"ios": null,
"desktop": null
},
"dateCreated": "2016-05-01T20:34:16+02:00",
"visitsSummary": {
"total": 1029,
@ -155,11 +150,6 @@
"shortCode": "12Kb3",
"shortUrl": "https://s.test/12Kb3",
"longUrl": "https://shlink.io",
"deviceLongUrls": {
"android": "https://shlink.io/android",
"ios": null,
"desktop": null
},
"dateCreated": "2016-05-01T20:34:16+02:00",
"visitsSummary": {
"total": 1029,

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];
}
}

View File

@ -250,18 +250,6 @@ class CreateShortUrlTest extends ApiTestCase
yield 'empty long url v3' => [['longUrl' => ' '], '3', 'https://shlink.io/api/error/invalid-data'];
yield 'missing url schema v2' => [['longUrl' => 'foo.com'], '2', 'https://shlink.io/api/error/invalid-data'];
yield 'missing url schema v3' => [['longUrl' => 'foo.com'], '3', 'https://shlink.io/api/error/invalid-data'];
yield 'empty device long url v2' => [[
'longUrl' => 'foo',
'deviceLongUrls' => [
'android' => null,
],
], '2', 'https://shlink.io/api/error/invalid-data'];
yield 'empty device long url v3' => [[
'longUrl' => 'foo',
'deviceLongUrls' => [
'ios' => ' ',
],
], '3', 'https://shlink.io/api/error/invalid-data'];
}
#[Test]
@ -313,22 +301,6 @@ class CreateShortUrlTest extends ApiTestCase
self::assertEquals('http://s.test/🦣🦣🦣', $payload['shortUrl']);
}
#[Test]
public function canCreateShortUrlsWithDeviceLongUrls(): void
{
[$statusCode, $payload] = $this->createShortUrl([
'longUrl' => 'https://github.com/shlinkio/shlink/issues/1557',
'deviceLongUrls' => [
'ios' => 'https://github.com/shlinkio/shlink/ios',
'android' => 'https://github.com/shlinkio/shlink/android',
],
]);
self::assertEquals(self::STATUS_OK, $statusCode);
self::assertEquals('https://github.com/shlinkio/shlink/ios', $payload['deviceLongUrls']['ios'] ?? null);
self::assertEquals('https://github.com/shlinkio/shlink/android', $payload['deviceLongUrls']['android'] ?? null);
}
#[Test]
public function titleIsIgnoredIfLongUrlTimesOut(): void
{

View File

@ -153,27 +153,4 @@ class EditShortUrlTest extends ApiTestCase
];
yield 'no domain' => [null, 'https://shlink.io/documentation/'];
}
#[Test]
public function deviceLongUrlsCanBeEdited(): void
{
$shortCode = 'def456';
$url = new Uri(sprintf('/short-urls/%s', $shortCode));
$editResp = $this->callApiWithKey(self::METHOD_PATCH, (string) $url, [RequestOptions::JSON => [
'deviceLongUrls' => [
'android' => null, // This one will get removed
'ios' => 'https://blog.alejandrocelaya.com/ios/edited', // This one will be edited
'desktop' => 'https://blog.alejandrocelaya.com/desktop', // This one is new and will be created
],
]]);
$deviceLongUrls = $this->getJsonResponsePayload($editResp)['deviceLongUrls'] ?? [];
self::assertEquals(self::STATUS_OK, $editResp->getStatusCode());
self::assertArrayHasKey('ios', $deviceLongUrls);
self::assertEquals('https://blog.alejandrocelaya.com/ios/edited', $deviceLongUrls['ios']);
self::assertArrayHasKey('desktop', $deviceLongUrls);
self::assertEquals('https://blog.alejandrocelaya.com/desktop', $deviceLongUrls['desktop']);
self::assertArrayHasKey('android', $deviceLongUrls);
self::assertNull($deviceLongUrls['android']);
}
}

View File

@ -8,7 +8,6 @@ use Cake\Chronos\Chronos;
use GuzzleHttp\RequestOptions;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use Shlinkio\Shlink\Core\Model\DeviceType;
use Shlinkio\Shlink\TestUtils\ApiTest\ApiTestCase;
use function count;
@ -163,123 +162,109 @@ class ListShortUrlsTest extends ApiTestCase
public static function provideFilteredLists(): iterable
{
$withDeviceLongUrls = static fn (array $shortUrl, ?array $longUrls = null) => [
...$shortUrl,
'deviceLongUrls' => $longUrls ?? [
DeviceType::ANDROID->value => null,
DeviceType::IOS->value => null,
DeviceType::DESKTOP->value => null,
],
];
$shortUrlMeta = $withDeviceLongUrls(self::SHORT_URL_META, [
DeviceType::ANDROID->value => 'https://blog.alejandrocelaya.com/android',
DeviceType::IOS->value => 'https://blog.alejandrocelaya.com/ios',
DeviceType::DESKTOP->value => null,
]);
yield [[], [
$withDeviceLongUrls(self::SHORT_URL_CUSTOM_DOMAIN),
$withDeviceLongUrls(self::SHORT_URL_CUSTOM_SLUG),
$shortUrlMeta,
$withDeviceLongUrls(self::SHORT_URL_CUSTOM_SLUG_AND_DOMAIN),
$withDeviceLongUrls(self::SHORT_URL_SHLINK_WITH_TITLE),
$withDeviceLongUrls(self::SHORT_URL_DOCS),
self::SHORT_URL_CUSTOM_DOMAIN,
self::SHORT_URL_CUSTOM_SLUG,
self::SHORT_URL_META,
self::SHORT_URL_CUSTOM_SLUG_AND_DOMAIN,
self::SHORT_URL_SHLINK_WITH_TITLE,
self::SHORT_URL_DOCS,
], 'valid_api_key'];
yield [['excludePastValidUntil' => 'true'], [
$withDeviceLongUrls(self::SHORT_URL_CUSTOM_DOMAIN),
$withDeviceLongUrls(self::SHORT_URL_CUSTOM_SLUG),
$shortUrlMeta,
$withDeviceLongUrls(self::SHORT_URL_CUSTOM_SLUG_AND_DOMAIN),
$withDeviceLongUrls(self::SHORT_URL_SHLINK_WITH_TITLE),
self::SHORT_URL_CUSTOM_DOMAIN,
self::SHORT_URL_CUSTOM_SLUG,
self::SHORT_URL_META,
self::SHORT_URL_CUSTOM_SLUG_AND_DOMAIN,
self::SHORT_URL_SHLINK_WITH_TITLE,
], 'valid_api_key'];
yield [['excludeMaxVisitsReached' => 'true'], [
$withDeviceLongUrls(self::SHORT_URL_CUSTOM_DOMAIN),
$withDeviceLongUrls(self::SHORT_URL_CUSTOM_SLUG),
$shortUrlMeta,
$withDeviceLongUrls(self::SHORT_URL_CUSTOM_SLUG_AND_DOMAIN),
$withDeviceLongUrls(self::SHORT_URL_DOCS),
self::SHORT_URL_CUSTOM_DOMAIN,
self::SHORT_URL_CUSTOM_SLUG,
self::SHORT_URL_META,
self::SHORT_URL_CUSTOM_SLUG_AND_DOMAIN,
self::SHORT_URL_DOCS,
], 'valid_api_key'];
yield [['orderBy' => 'shortCode'], [
$withDeviceLongUrls(self::SHORT_URL_SHLINK_WITH_TITLE),
$withDeviceLongUrls(self::SHORT_URL_CUSTOM_SLUG),
$withDeviceLongUrls(self::SHORT_URL_CUSTOM_SLUG_AND_DOMAIN),
$shortUrlMeta,
$withDeviceLongUrls(self::SHORT_URL_DOCS),
$withDeviceLongUrls(self::SHORT_URL_CUSTOM_DOMAIN),
self::SHORT_URL_SHLINK_WITH_TITLE,
self::SHORT_URL_CUSTOM_SLUG,
self::SHORT_URL_CUSTOM_SLUG_AND_DOMAIN,
self::SHORT_URL_META,
self::SHORT_URL_DOCS,
self::SHORT_URL_CUSTOM_DOMAIN,
], 'valid_api_key'];
yield [['orderBy' => 'shortCode-DESC'], [
$withDeviceLongUrls(self::SHORT_URL_DOCS),
$withDeviceLongUrls(self::SHORT_URL_CUSTOM_DOMAIN),
$shortUrlMeta,
$withDeviceLongUrls(self::SHORT_URL_CUSTOM_SLUG_AND_DOMAIN),
$withDeviceLongUrls(self::SHORT_URL_CUSTOM_SLUG),
$withDeviceLongUrls(self::SHORT_URL_SHLINK_WITH_TITLE),
self::SHORT_URL_DOCS,
self::SHORT_URL_CUSTOM_DOMAIN,
self::SHORT_URL_META,
self::SHORT_URL_CUSTOM_SLUG_AND_DOMAIN,
self::SHORT_URL_CUSTOM_SLUG,
self::SHORT_URL_SHLINK_WITH_TITLE,
], 'valid_api_key'];
yield [['orderBy' => 'title-DESC'], [
$shortUrlMeta,
$withDeviceLongUrls(self::SHORT_URL_CUSTOM_SLUG),
$withDeviceLongUrls(self::SHORT_URL_DOCS),
$withDeviceLongUrls(self::SHORT_URL_CUSTOM_DOMAIN),
$withDeviceLongUrls(self::SHORT_URL_CUSTOM_SLUG_AND_DOMAIN),
$withDeviceLongUrls(self::SHORT_URL_SHLINK_WITH_TITLE),
self::SHORT_URL_META,
self::SHORT_URL_CUSTOM_SLUG,
self::SHORT_URL_DOCS,
self::SHORT_URL_CUSTOM_DOMAIN,
self::SHORT_URL_CUSTOM_SLUG_AND_DOMAIN,
self::SHORT_URL_SHLINK_WITH_TITLE,
], 'valid_api_key'];
yield [['startDate' => Chronos::parse('2018-12-01')->toAtomString()], [
$withDeviceLongUrls(self::SHORT_URL_CUSTOM_DOMAIN),
$withDeviceLongUrls(self::SHORT_URL_CUSTOM_SLUG),
$shortUrlMeta,
self::SHORT_URL_CUSTOM_DOMAIN,
self::SHORT_URL_CUSTOM_SLUG,
self::SHORT_URL_META,
], 'valid_api_key'];
yield [['endDate' => Chronos::parse('2018-12-01')->toAtomString()], [
$withDeviceLongUrls(self::SHORT_URL_CUSTOM_SLUG_AND_DOMAIN),
$withDeviceLongUrls(self::SHORT_URL_SHLINK_WITH_TITLE),
$withDeviceLongUrls(self::SHORT_URL_DOCS),
self::SHORT_URL_CUSTOM_SLUG_AND_DOMAIN,
self::SHORT_URL_SHLINK_WITH_TITLE,
self::SHORT_URL_DOCS,
], 'valid_api_key'];
yield [['tags' => ['foo']], [
$withDeviceLongUrls(self::SHORT_URL_CUSTOM_DOMAIN),
$shortUrlMeta,
$withDeviceLongUrls(self::SHORT_URL_SHLINK_WITH_TITLE),
self::SHORT_URL_CUSTOM_DOMAIN,
self::SHORT_URL_META,
self::SHORT_URL_SHLINK_WITH_TITLE,
], 'valid_api_key'];
yield [['tags' => ['bar']], [
$shortUrlMeta,
self::SHORT_URL_META,
], 'valid_api_key'];
yield [['tags' => ['foo', 'bar']], [
$withDeviceLongUrls(self::SHORT_URL_CUSTOM_DOMAIN),
$shortUrlMeta,
$withDeviceLongUrls(self::SHORT_URL_SHLINK_WITH_TITLE),
self::SHORT_URL_CUSTOM_DOMAIN,
self::SHORT_URL_META,
self::SHORT_URL_SHLINK_WITH_TITLE,
], 'valid_api_key'];
yield [['tags' => ['foo', 'bar'], 'tagsMode' => 'any'], [
$withDeviceLongUrls(self::SHORT_URL_CUSTOM_DOMAIN),
$shortUrlMeta,
$withDeviceLongUrls(self::SHORT_URL_SHLINK_WITH_TITLE),
self::SHORT_URL_CUSTOM_DOMAIN,
self::SHORT_URL_META,
self::SHORT_URL_SHLINK_WITH_TITLE,
], 'valid_api_key'];
yield [['tags' => ['foo', 'bar'], 'tagsMode' => 'all'], [
$shortUrlMeta,
self::SHORT_URL_META,
], 'valid_api_key'];
yield [['tags' => ['foo', 'bar', 'baz']], [
$withDeviceLongUrls(self::SHORT_URL_CUSTOM_DOMAIN),
$shortUrlMeta,
$withDeviceLongUrls(self::SHORT_URL_SHLINK_WITH_TITLE),
self::SHORT_URL_CUSTOM_DOMAIN,
self::SHORT_URL_META,
self::SHORT_URL_SHLINK_WITH_TITLE,
], 'valid_api_key'];
yield [['tags' => ['foo', 'bar', 'baz'], 'tagsMode' => 'all'], [], 'valid_api_key'];
yield [['tags' => ['foo'], 'endDate' => Chronos::parse('2018-12-01')->toAtomString()], [
$withDeviceLongUrls(self::SHORT_URL_SHLINK_WITH_TITLE),
self::SHORT_URL_SHLINK_WITH_TITLE,
], 'valid_api_key'];
yield [['searchTerm' => 'alejandro'], [
$withDeviceLongUrls(self::SHORT_URL_CUSTOM_DOMAIN),
$shortUrlMeta,
self::SHORT_URL_CUSTOM_DOMAIN,
self::SHORT_URL_META,
], 'valid_api_key'];
yield [['searchTerm' => 'cool'], [
$withDeviceLongUrls(self::SHORT_URL_SHLINK_WITH_TITLE),
self::SHORT_URL_SHLINK_WITH_TITLE,
], 'valid_api_key'];
yield [['searchTerm' => 'example.com'], [
$withDeviceLongUrls(self::SHORT_URL_CUSTOM_DOMAIN),
self::SHORT_URL_CUSTOM_DOMAIN,
], 'valid_api_key'];
yield [[], [
$withDeviceLongUrls(self::SHORT_URL_CUSTOM_SLUG),
$shortUrlMeta,
$withDeviceLongUrls(self::SHORT_URL_SHLINK_WITH_TITLE),
self::SHORT_URL_CUSTOM_SLUG,
self::SHORT_URL_META,
self::SHORT_URL_SHLINK_WITH_TITLE,
], 'author_api_key'];
yield [[], [
$withDeviceLongUrls(self::SHORT_URL_CUSTOM_DOMAIN),
self::SHORT_URL_CUSTOM_DOMAIN,
], 'domain_api_key'];
}

View File

@ -8,6 +8,7 @@ use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\DataFixtures\AbstractFixture;
use Doctrine\Common\DataFixtures\DependentFixtureInterface;
use Doctrine\Persistence\ObjectManager;
use Shlinkio\Shlink\Core\Model\DeviceType;
use Shlinkio\Shlink\Core\RedirectRule\Entity\RedirectCondition;
use Shlinkio\Shlink\Core\RedirectRule\Entity\ShortUrlRedirectRule;
use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
@ -33,6 +34,12 @@ class ShortUrlRedirectRulesFixture extends AbstractFixture implements DependentF
$helloQueryCondition = RedirectCondition::forQueryParam('hello', 'world');
$manager->persist($helloQueryCondition);
$androidCondition = RedirectCondition::forDevice(DeviceType::ANDROID);
$manager->persist($androidCondition);
$iosCondition = RedirectCondition::forDevice(DeviceType::IOS);
$manager->persist($iosCondition);
$englishAndFooQueryRule = new ShortUrlRedirectRule(
$defShortUrl,
1,
@ -57,6 +64,22 @@ class ShortUrlRedirectRulesFixture extends AbstractFixture implements DependentF
);
$manager->persist($onlyEnglishRule);
$androidRule = new ShortUrlRedirectRule(
$defShortUrl,
4,
'https://blog.alejandrocelaya.com/android',
new ArrayCollection([$androidCondition]),
);
$manager->persist($androidRule);
$iosRule = new ShortUrlRedirectRule(
$defShortUrl,
5,
'https://blog.alejandrocelaya.com/ios',
new ArrayCollection([$iosCondition]),
);
$manager->persist($iosRule);
$manager->flush();
}
}

View File

@ -9,7 +9,6 @@ use Doctrine\Common\DataFixtures\AbstractFixture;
use Doctrine\Common\DataFixtures\DependentFixtureInterface;
use Doctrine\Persistence\ObjectManager;
use ReflectionObject;
use Shlinkio\Shlink\Core\Model\DeviceType;
use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlCreation;
use Shlinkio\Shlink\Core\ShortUrl\Resolver\PersistenceShortUrlRelationResolver;
@ -49,10 +48,6 @@ class ShortUrlsFixture extends AbstractFixture implements DependentFixtureInterf
'apiKey' => $authorApiKey,
'longUrl' =>
'https://blog.alejandrocelaya.com/2017/12/09/acmailer-7-0-the-most-important-release-in-a-long-time/',
'deviceLongUrls' => [
DeviceType::ANDROID->value => 'https://blog.alejandrocelaya.com/android',
DeviceType::IOS->value => 'https://blog.alejandrocelaya.com/ios',
],
'tags' => ['foo', 'bar'],
]), $relationResolver), '2019-01-01 00:00:10');
$manager->persist($defShortUrl);