Add deviceLongUrls to short URL creation

This commit is contained in:
Alejandro Celaya 2023-01-14 15:19:47 +01:00
parent 12150f775d
commit 1447687ebe
23 changed files with 222 additions and 229 deletions

View file

@ -100,9 +100,8 @@ class CreateShortUrlCommandTest extends TestCase
{
$shortUrl = ShortUrl::createEmpty();
$this->urlShortener->expects($this->once())->method('shorten')->with(
$this->callback(function (ShortUrlCreation $meta) {
$tags = $meta->getTags();
Assert::assertEquals(['foo', 'bar', 'baz', 'boo', 'zar'], $tags);
$this->callback(function (ShortUrlCreation $creation) {
Assert::assertEquals(['foo', 'bar', 'baz', 'boo', 'zar'], $creation->tags);
return true;
}),
)->willReturn($shortUrl);
@ -128,7 +127,7 @@ class CreateShortUrlCommandTest extends TestCase
{
$this->urlShortener->expects($this->once())->method('shorten')->with(
$this->callback(function (ShortUrlCreation $meta) use ($expectedDomain) {
Assert::assertEquals($expectedDomain, $meta->getDomain());
Assert::assertEquals($expectedDomain, $meta->domain);
return true;
}),
)->willReturn(ShortUrl::createEmpty());

View file

@ -9,10 +9,20 @@ use Shlinkio\Shlink\Core\Model\DeviceType;
class DeviceLongUrl extends AbstractEntity
{
private function __construct(
public function __construct(
public readonly ShortUrl $shortUrl,
public readonly DeviceType $deviceType,
public readonly string $longUrl,
private string $longUrl,
) {
}
public function longUrl(): string
{
return $this->longUrl;
}
public function updateLongUrl(string $longUrl): void
{
$this->longUrl = $longUrl;
}
}

View file

@ -60,6 +60,9 @@ class ShortUrl extends AbstractEntity
return self::create(ShortUrlCreation::createEmpty());
}
/**
* @param non-empty-string $longUrl
*/
public static function withLongUrl(string $longUrl): self
{
return self::create(ShortUrlCreation::fromRawData([ShortUrlInputFilter::LONG_URL => $longUrl]));
@ -75,19 +78,19 @@ class ShortUrl extends AbstractEntity
$instance->longUrl = $creation->getLongUrl();
$instance->dateCreated = Chronos::now();
$instance->visits = new ArrayCollection();
$instance->tags = $relationResolver->resolveTags($creation->getTags());
$instance->validSince = $creation->getValidSince();
$instance->validUntil = $creation->getValidUntil();
$instance->maxVisits = $creation->getMaxVisits();
$instance->tags = $relationResolver->resolveTags($creation->tags);
$instance->validSince = $creation->validSince;
$instance->validUntil = $creation->validUntil;
$instance->maxVisits = $creation->maxVisits;
$instance->customSlugWasProvided = $creation->hasCustomSlug();
$instance->shortCodeLength = $creation->getShortCodeLength();
$instance->shortCode = $creation->getCustomSlug() ?? generateRandomShortCode($instance->shortCodeLength);
$instance->domain = $relationResolver->resolveDomain($creation->getDomain());
$instance->authorApiKey = $creation->getApiKey();
$instance->title = $creation->getTitle();
$instance->titleWasAutoResolved = $creation->titleWasAutoResolved();
$instance->crawlable = $creation->isCrawlable();
$instance->forwardQuery = $creation->forwardQuery();
$instance->shortCodeLength = $creation->shortCodeLength;
$instance->shortCode = $creation->customSlug ?? generateRandomShortCode($instance->shortCodeLength);
$instance->domain = $relationResolver->resolveDomain($creation->domain);
$instance->authorApiKey = $creation->apiKey;
$instance->title = $creation->title;
$instance->titleWasAutoResolved = $creation->titleWasAutoResolved;
$instance->crawlable = $creation->crawlable;
$instance->forwardQuery = $creation->forwardQuery;
return $instance;
}

View file

@ -6,85 +6,106 @@ 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;
use Shlinkio\Shlink\Rest\Entity\ApiKey;
use function Functional\map;
use function Shlinkio\Shlink\Core\getNonEmptyOptionalValueFromInputFilter;
use function Shlinkio\Shlink\Core\getOptionalBoolFromInputFilter;
use function Shlinkio\Shlink\Core\getOptionalIntFromInputFilter;
use function Shlinkio\Shlink\Core\normalizeOptionalDate;
use function trim;
use const Shlinkio\Shlink\DEFAULT_SHORT_CODES_LENGTH;
final class ShortUrlCreation implements TitleResolutionModelInterface
{
private string $longUrl;
private ?Chronos $validSince = null;
private ?Chronos $validUntil = null;
private ?string $customSlug = null;
private ?int $maxVisits = null;
private ?bool $findIfExists = null;
private ?string $domain = null;
private int $shortCodeLength = 5;
private bool $validateUrl = false;
private ?ApiKey $apiKey = null;
private array $tags = [];
private ?string $title = null;
private bool $titleWasAutoResolved = false;
private bool $crawlable = false;
private bool $forwardQuery = true;
private function __construct()
{
/**
* @param string[] $tags
* @param array{DeviceType, string}[] $deviceLongUrls
*/
private function __construct(
public readonly string $longUrl,
public readonly array $deviceLongUrls = [],
public readonly ?Chronos $validSince = null,
public readonly ?Chronos $validUntil = null,
public readonly ?string $customSlug = null,
public readonly ?int $maxVisits = null,
public readonly bool $findIfExists = false,
public readonly ?string $domain = null,
public readonly int $shortCodeLength = 5,
public readonly bool $validateUrl = false,
public readonly ?ApiKey $apiKey = null,
public readonly array $tags = [],
public readonly ?string $title = null,
public readonly bool $titleWasAutoResolved = false,
public readonly bool $crawlable = false,
public readonly bool $forwardQuery = true,
) {
}
public static function createEmpty(): self
{
$instance = new self();
$instance->longUrl = '';
return $instance;
return new self('');
}
/**
* @throws ValidationException
*/
public static function fromRawData(array $data): self
{
$instance = new self();
$instance->validateAndInit($data);
return $instance;
}
/**
* @throws ValidationException
*/
private function validateAndInit(array $data): void
{
$inputFilter = ShortUrlInputFilter::withRequiredLongUrl($data);
if (! $inputFilter->isValid()) {
throw ValidationException::fromInputFilter($inputFilter);
}
$this->longUrl = $inputFilter->getValue(ShortUrlInputFilter::LONG_URL);
$this->validSince = normalizeOptionalDate($inputFilter->getValue(ShortUrlInputFilter::VALID_SINCE));
$this->validUntil = normalizeOptionalDate($inputFilter->getValue(ShortUrlInputFilter::VALID_UNTIL));
$this->customSlug = $inputFilter->getValue(ShortUrlInputFilter::CUSTOM_SLUG);
$this->maxVisits = getOptionalIntFromInputFilter($inputFilter, ShortUrlInputFilter::MAX_VISITS);
$this->findIfExists = $inputFilter->getValue(ShortUrlInputFilter::FIND_IF_EXISTS);
$this->validateUrl = getOptionalBoolFromInputFilter($inputFilter, ShortUrlInputFilter::VALIDATE_URL) ?? false;
$this->domain = getNonEmptyOptionalValueFromInputFilter($inputFilter, ShortUrlInputFilter::DOMAIN);
$this->shortCodeLength = getOptionalIntFromInputFilter(
$inputFilter,
ShortUrlInputFilter::SHORT_CODE_LENGTH,
) ?? DEFAULT_SHORT_CODES_LENGTH;
$this->apiKey = $inputFilter->getValue(ShortUrlInputFilter::API_KEY);
$this->tags = $inputFilter->getValue(ShortUrlInputFilter::TAGS);
$this->title = $inputFilter->getValue(ShortUrlInputFilter::TITLE);
$this->crawlable = $inputFilter->getValue(ShortUrlInputFilter::CRAWLABLE);
$this->forwardQuery = getOptionalBoolFromInputFilter($inputFilter, ShortUrlInputFilter::FORWARD_QUERY) ?? true;
return new self(
longUrl: $inputFilter->getValue(ShortUrlInputFilter::LONG_URL),
deviceLongUrls: map(
$inputFilter->getValue(ShortUrlInputFilter::DEVICE_LONG_URLS) ?? [],
static fn (string $longUrl, string $deviceType) => [DeviceType::from($deviceType), trim($longUrl)],
),
validSince: normalizeOptionalDate($inputFilter->getValue(ShortUrlInputFilter::VALID_SINCE)),
validUntil: normalizeOptionalDate($inputFilter->getValue(ShortUrlInputFilter::VALID_UNTIL)),
customSlug: $inputFilter->getValue(ShortUrlInputFilter::CUSTOM_SLUG),
maxVisits: getOptionalIntFromInputFilter($inputFilter, ShortUrlInputFilter::MAX_VISITS),
findIfExists: $inputFilter->getValue(ShortUrlInputFilter::FIND_IF_EXISTS) ?? false,
domain: getNonEmptyOptionalValueFromInputFilter($inputFilter, ShortUrlInputFilter::DOMAIN),
shortCodeLength: getOptionalIntFromInputFilter(
$inputFilter,
ShortUrlInputFilter::SHORT_CODE_LENGTH,
) ?? DEFAULT_SHORT_CODES_LENGTH,
validateUrl: getOptionalBoolFromInputFilter($inputFilter, ShortUrlInputFilter::VALIDATE_URL) ?? false,
apiKey: $inputFilter->getValue(ShortUrlInputFilter::API_KEY),
tags: $inputFilter->getValue(ShortUrlInputFilter::TAGS),
title: $inputFilter->getValue(ShortUrlInputFilter::TITLE),
crawlable: $inputFilter->getValue(ShortUrlInputFilter::CRAWLABLE),
forwardQuery: getOptionalBoolFromInputFilter($inputFilter, ShortUrlInputFilter::FORWARD_QUERY) ?? true,
);
}
public function withResolvedTitle(string $title): self
{
return new self(
$this->longUrl,
$this->deviceLongUrls,
$this->validSince,
$this->validUntil,
$this->customSlug,
$this->maxVisits,
$this->findIfExists,
$this->domain,
$this->shortCodeLength,
$this->validateUrl,
$this->apiKey,
$this->tags,
$title,
true,
$this->crawlable,
$this->forwardQuery,
);
}
public function getLongUrl(): string
@ -92,115 +113,38 @@ final class ShortUrlCreation implements TitleResolutionModelInterface
return $this->longUrl;
}
public function getValidSince(): ?Chronos
{
return $this->validSince;
}
public function hasValidSince(): bool
{
return $this->validSince !== null;
}
public function getValidUntil(): ?Chronos
{
return $this->validUntil;
}
public function hasValidUntil(): bool
{
return $this->validUntil !== null;
}
public function getCustomSlug(): ?string
{
return $this->customSlug;
}
public function hasCustomSlug(): bool
{
return $this->customSlug !== null;
}
public function getMaxVisits(): ?int
{
return $this->maxVisits;
}
public function hasMaxVisits(): bool
{
return $this->maxVisits !== null;
}
public function findIfExists(): bool
{
return (bool) $this->findIfExists;
}
public function hasDomain(): bool
{
return $this->domain !== null;
}
public function getDomain(): ?string
{
return $this->domain;
}
public function getShortCodeLength(): int
{
return $this->shortCodeLength;
}
public function doValidateUrl(): bool
{
return $this->validateUrl;
}
public function getApiKey(): ?ApiKey
{
return $this->apiKey;
}
/**
* @return string[]
*/
public function getTags(): array
{
return $this->tags;
}
public function getTitle(): ?string
{
return $this->title;
}
public function hasTitle(): bool
{
return $this->title !== null;
}
public function titleWasAutoResolved(): bool
{
return $this->titleWasAutoResolved;
}
public function withResolvedTitle(string $title): self
{
$copy = clone $this;
$copy->title = $title;
$copy->titleWasAutoResolved = true;
return $copy;
}
public function isCrawlable(): bool
{
return $this->crawlable;
}
public function forwardQuery(): bool
{
return $this->forwardQuery;
}
}

View file

@ -6,14 +6,20 @@ namespace Shlinkio\Shlink\Core\ShortUrl\Model\Validation;
use DateTime;
use Laminas\Filter;
use Laminas\InputFilter\Input;
use Laminas\InputFilter\InputFilter;
use Laminas\Validator;
use Shlinkio\Shlink\Common\Validation;
use Shlinkio\Shlink\Core\Config\EnvVars;
use Shlinkio\Shlink\Core\Model\DeviceType;
use Shlinkio\Shlink\Rest\Entity\ApiKey;
use function array_keys;
use function array_values;
use function Functional\contains;
use function Functional\every;
use function is_array;
use function is_string;
use function Shlinkio\Shlink\Core\enumValues;
use function str_replace;
use function substr;
use function trim;
@ -32,6 +38,7 @@ class ShortUrlInputFilter extends InputFilter
public const DOMAIN = 'domain';
public const SHORT_CODE_LENGTH = 'shortCodeLength';
public const LONG_URL = 'longUrl';
public const DEVICE_LONG_URLS = 'deviceLongUrls';
public const VALIDATE_URL = 'validateUrl';
public const API_KEY = 'apiKey';
public const TAGS = 'tags';
@ -57,16 +64,40 @@ class ShortUrlInputFilter extends InputFilter
private function initialize(bool $requireLongUrl, bool $multiSegmentEnabled): void
{
$longUrlInput = $this->createInput(self::LONG_URL, $requireLongUrl);
$longUrlInput->getValidatorChain()->attach(new Validator\NotEmpty([
$notEmptyValidator = new Validator\NotEmpty([
Validator\NotEmpty::OBJECT,
Validator\NotEmpty::SPACE,
Validator\NotEmpty::NULL,
Validator\NotEmpty::EMPTY_ARRAY,
Validator\NotEmpty::BOOLEAN,
]));
Validator\NotEmpty::STRING,
]);
$longUrlInput = $this->createInput(self::LONG_URL, $requireLongUrl);
$longUrlInput->getValidatorChain()->attach($notEmptyValidator);
$this->add($longUrlInput);
$deviceLongUrlsInput = $this->createInput(self::DEVICE_LONG_URLS, false);
$deviceLongUrlsInput->getValidatorChain()->attach(
new Validator\Callback(function (mixed $value) use ($notEmptyValidator): bool {
if (! is_array($value)) {
// TODO Set proper error: Not array
return false;
}
$validValues = enumValues(DeviceType::class);
$keys = array_keys($value);
if (! every($keys, static fn ($key) => contains($validValues, $key))) {
// TODO Set proper error: Provided invalid device type
return false;
}
$longUrls = array_values($value);
return every($longUrls, $notEmptyValidator->isValid(...));
}),
);
$this->add($deviceLongUrlsInput);
$validSince = $this->createInput(self::VALID_SINCE, false);
$validSince->getValidatorChain()->attach(new Validator\Date(['format' => DateTime::ATOM]));
$this->add($validSince);
@ -75,8 +106,8 @@ class ShortUrlInputFilter extends InputFilter
$validUntil->getValidatorChain()->attach(new Validator\Date(['format' => DateTime::ATOM]));
$this->add($validUntil);
// FIXME The only way to enforce the NotEmpty validator to be evaluated when the value is provided but it's
// empty, is by using the deprecated setContinueIfEmpty
// The only way to enforce the NotEmpty validator to be evaluated when the key is present with an empty value
// is by using the deprecated setContinueIfEmpty
$customSlug = $this->createInput(self::CUSTOM_SLUG, false)->setContinueIfEmpty(true);
$customSlug->getFilterChain()->attach(new Filter\Callback(match ($multiSegmentEnabled) {
true => static fn (mixed $v) => is_string($v) ? trim(str_replace(' ', '-', $v), '/') : $v,
@ -102,10 +133,8 @@ class ShortUrlInputFilter extends InputFilter
$domain->getValidatorChain()->attach(new Validation\HostAndPortValidator());
$this->add($domain);
$apiKeyInput = new Input(self::API_KEY);
$apiKeyInput
->setRequired(false)
->getValidatorChain()->attach(new Validator\IsInstanceOf(['className' => ApiKey::class]));
$apiKeyInput = $this->createInput(self::API_KEY, false);
$apiKeyInput->getValidatorChain()->attach(new Validator\IsInstanceOf(['className' => ApiKey::class]));
$this->add($apiKeyInput);
$this->add($this->createTagsInput(self::TAGS, false));

View file

@ -101,45 +101,45 @@ class ShortUrlRepository extends EntitySpecificationRepository implements ShortU
return $qb;
}
public function findOneMatching(ShortUrlCreation $meta): ?ShortUrl
public function findOneMatching(ShortUrlCreation $creation): ?ShortUrl
{
$qb = $this->getEntityManager()->createQueryBuilder();
$qb->select('s')
->from(ShortUrl::class, 's')
->where($qb->expr()->eq('s.longUrl', ':longUrl'))
->setParameter('longUrl', $meta->getLongUrl())
->setParameter('longUrl', $creation->longUrl)
->setMaxResults(1)
->orderBy('s.id');
if ($meta->hasCustomSlug()) {
if ($creation->hasCustomSlug()) {
$qb->andWhere($qb->expr()->eq('s.shortCode', ':slug'))
->setParameter('slug', $meta->getCustomSlug());
->setParameter('slug', $creation->customSlug);
}
if ($meta->hasMaxVisits()) {
if ($creation->hasMaxVisits()) {
$qb->andWhere($qb->expr()->eq('s.maxVisits', ':maxVisits'))
->setParameter('maxVisits', $meta->getMaxVisits());
->setParameter('maxVisits', $creation->maxVisits);
}
if ($meta->hasValidSince()) {
if ($creation->hasValidSince()) {
$qb->andWhere($qb->expr()->eq('s.validSince', ':validSince'))
->setParameter('validSince', $meta->getValidSince(), ChronosDateTimeType::CHRONOS_DATETIME);
->setParameter('validSince', $creation->validSince, ChronosDateTimeType::CHRONOS_DATETIME);
}
if ($meta->hasValidUntil()) {
if ($creation->hasValidUntil()) {
$qb->andWhere($qb->expr()->eq('s.validUntil', ':validUntil'))
->setParameter('validUntil', $meta->getValidUntil(), ChronosDateTimeType::CHRONOS_DATETIME);
->setParameter('validUntil', $creation->validUntil, ChronosDateTimeType::CHRONOS_DATETIME);
}
if ($meta->hasDomain()) {
if ($creation->hasDomain()) {
$qb->join('s.domain', 'd')
->andWhere($qb->expr()->eq('d.authority', ':domain'))
->setParameter('domain', $meta->getDomain());
->setParameter('domain', $creation->domain);
}
$apiKey = $meta->getApiKey();
$apiKey = $creation->apiKey;
if ($apiKey !== null) {
$this->applySpecification($qb, $apiKey->spec(), 's');
}
$tags = $meta->getTags();
$tags = $creation->tags;
$tagsAmount = count($tags);
if ($tagsAmount === 0) {
return $qb->getQuery()->getOneOrNullResult();

View file

@ -22,7 +22,7 @@ interface ShortUrlRepositoryInterface extends ObjectRepository, EntitySpecificat
public function shortCodeIsInUseWithLock(ShortUrlIdentifier $identifier, ?Specification $spec = null): bool;
public function findOneMatching(ShortUrlCreation $meta): ?ShortUrl;
public function findOneMatching(ShortUrlCreation $creation): ?ShortUrl;
public function findOneByImportedUrl(ImportedShlinkUrl $url): ?ShortUrl;
}

View file

@ -57,15 +57,15 @@ class UrlShortener implements UrlShortenerInterface
return $newShortUrl;
}
private function findExistingShortUrlIfExists(ShortUrlCreation $meta): ?ShortUrl
private function findExistingShortUrlIfExists(ShortUrlCreation $creation): ?ShortUrl
{
if (! $meta->findIfExists()) {
if (! $creation->findIfExists) {
return null;
}
/** @var ShortUrlRepositoryInterface $repo */
$repo = $this->em->getRepository(ShortUrl::class);
return $repo->findOneMatching($meta);
return $repo->findOneMatching($creation);
}
private function verifyShortCodeUniqueness(ShortUrlCreation $meta, ShortUrl $shortUrlToBeCreated): void

View file

@ -75,7 +75,7 @@ class TagRepositoryTest extends DatabaseTestCase
[$firstUrlTags] = array_chunk($names, 3);
$secondUrlTags = [$names[0]];
$metaWithTags = static fn (array $tags, ?ApiKey $apiKey) => ShortUrlCreation::fromRawData(
['longUrl' => '', 'tags' => $tags, 'apiKey' => $apiKey],
['longUrl' => 'longUrl', 'tags' => $tags, 'apiKey' => $apiKey],
);
$shortUrl = ShortUrl::create($metaWithTags($firstUrlTags, $apiKey), $this->relationResolver);
@ -242,14 +242,14 @@ class TagRepositoryTest extends DatabaseTestCase
[$firstUrlTags, $secondUrlTags] = array_chunk($names, 3);
$shortUrl = ShortUrl::create(
ShortUrlCreation::fromRawData(['apiKey' => $authorApiKey, 'longUrl' => '', 'tags' => $firstUrlTags]),
ShortUrlCreation::fromRawData(['apiKey' => $authorApiKey, 'longUrl' => 'longUrl', 'tags' => $firstUrlTags]),
$this->relationResolver,
);
$this->getEntityManager()->persist($shortUrl);
$shortUrl2 = ShortUrl::create(
ShortUrlCreation::fromRawData(
['domain' => $domain->getAuthority(), 'longUrl' => '', 'tags' => $secondUrlTags],
['domain' => $domain->getAuthority(), 'longUrl' => 'longUrl', 'tags' => $secondUrlTags],
),
$this->relationResolver,
);

View file

@ -264,7 +264,9 @@ class VisitRepositoryTest extends DatabaseTestCase
$apiKey1 = ApiKey::fromMeta(ApiKeyMeta::withRoles(RoleDefinition::forAuthoredShortUrls()));
$this->getEntityManager()->persist($apiKey1);
$shortUrl = ShortUrl::create(
ShortUrlCreation::fromRawData(['apiKey' => $apiKey1, 'domain' => $domain->getAuthority(), 'longUrl' => '']),
ShortUrlCreation::fromRawData(
['apiKey' => $apiKey1, 'domain' => $domain->getAuthority(), 'longUrl' => 'longUrl'],
),
$this->relationResolver,
);
$this->getEntityManager()->persist($shortUrl);
@ -272,12 +274,14 @@ class VisitRepositoryTest extends DatabaseTestCase
$apiKey2 = ApiKey::fromMeta(ApiKeyMeta::withRoles(RoleDefinition::forAuthoredShortUrls()));
$this->getEntityManager()->persist($apiKey2);
$shortUrl2 = ShortUrl::create(ShortUrlCreation::fromRawData(['apiKey' => $apiKey2, 'longUrl' => '']));
$shortUrl2 = ShortUrl::create(ShortUrlCreation::fromRawData(['apiKey' => $apiKey2, 'longUrl' => 'longUrl']));
$this->getEntityManager()->persist($shortUrl2);
$this->createVisitsForShortUrl($shortUrl2, 5);
$shortUrl3 = ShortUrl::create(
ShortUrlCreation::fromRawData(['apiKey' => $apiKey2, 'domain' => $domain->getAuthority(), 'longUrl' => '']),
ShortUrlCreation::fromRawData(
['apiKey' => $apiKey2, 'domain' => $domain->getAuthority(), 'longUrl' => 'longUrl'],
),
$this->relationResolver,
);
$this->getEntityManager()->persist($shortUrl3);
@ -315,7 +319,7 @@ class VisitRepositoryTest extends DatabaseTestCase
/** @test */
public function findOrphanVisitsReturnsExpectedResult(): void
{
$shortUrl = ShortUrl::create(ShortUrlCreation::fromRawData(['longUrl' => '']));
$shortUrl = ShortUrl::create(ShortUrlCreation::fromRawData(['longUrl' => 'longUrl']));
$this->getEntityManager()->persist($shortUrl);
$this->createVisitsForShortUrl($shortUrl, 7);
@ -364,7 +368,7 @@ class VisitRepositoryTest extends DatabaseTestCase
/** @test */
public function countOrphanVisitsReturnsExpectedResult(): void
{
$shortUrl = ShortUrl::create(ShortUrlCreation::fromRawData(['longUrl' => '']));
$shortUrl = ShortUrl::create(ShortUrlCreation::fromRawData(['longUrl' => 'longUrl']));
$this->getEntityManager()->persist($shortUrl);
$this->createVisitsForShortUrl($shortUrl, 7);
@ -460,7 +464,7 @@ class VisitRepositoryTest extends DatabaseTestCase
}
/**
* @return array{string, string, \Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl}
* @return array{string, string, ShortUrl}
*/
private function createShortUrlsAndVisits(
bool|string $withDomain = true,
@ -468,7 +472,7 @@ class VisitRepositoryTest extends DatabaseTestCase
?ApiKey $apiKey = null,
): array {
$shortUrl = ShortUrl::create(ShortUrlCreation::fromRawData([
ShortUrlInputFilter::LONG_URL => '',
ShortUrlInputFilter::LONG_URL => 'longUrl',
ShortUrlInputFilter::TAGS => $tags,
ShortUrlInputFilter::API_KEY => $apiKey,
]), $this->relationResolver);
@ -482,7 +486,7 @@ class VisitRepositoryTest extends DatabaseTestCase
$shortUrlWithDomain = ShortUrl::create(ShortUrlCreation::fromRawData([
'customSlug' => $shortCode,
'domain' => $domain,
'longUrl' => '',
'longUrl' => 'longUrl',
]));
$this->getEntityManager()->persist($shortUrlWithDomain);
$this->createVisitsForShortUrl($shortUrlWithDomain, 3);

View file

@ -57,7 +57,7 @@ class NotifyNewShortUrlToMercureTest extends TestCase
/** @test */
public function expectedNotificationIsPublished(): void
{
$shortUrl = ShortUrl::withLongUrl('');
$shortUrl = ShortUrl::withLongUrl('longUrl');
$update = Update::forTopicAndPayload('', []);
$this->em->expects($this->once())->method('find')->with(ShortUrl::class, '123')->willReturn($shortUrl);
@ -74,7 +74,7 @@ class NotifyNewShortUrlToMercureTest extends TestCase
/** @test */
public function messageIsPrintedIfPublishingFails(): void
{
$shortUrl = ShortUrl::withLongUrl('');
$shortUrl = ShortUrl::withLongUrl('longUrl');
$update = Update::forTopicAndPayload('', []);
$e = new Exception('Error');

View file

@ -38,7 +38,7 @@ class PublishingUpdatesGeneratorTest extends TestCase
{
$shortUrl = ShortUrl::create(ShortUrlCreation::fromRawData([
'customSlug' => 'foo',
'longUrl' => '',
'longUrl' => 'longUrl',
'title' => $title,
]));
$visit = Visit::forValidShortUrl($shortUrl, Visitor::emptyInstance());
@ -51,7 +51,7 @@ class PublishingUpdatesGeneratorTest extends TestCase
'shortUrl' => [
'shortCode' => $shortUrl->getShortCode(),
'shortUrl' => 'http:/' . $shortUrl->getShortCode(),
'longUrl' => '',
'longUrl' => 'longUrl',
'dateCreated' => $shortUrl->getDateCreated()->toAtomString(),
'visitsCount' => 0,
'tags' => [],
@ -118,7 +118,7 @@ class PublishingUpdatesGeneratorTest extends TestCase
{
$shortUrl = ShortUrl::create(ShortUrlCreation::fromRawData([
'customSlug' => 'foo',
'longUrl' => '',
'longUrl' => 'longUrl',
'title' => 'The title',
]));
@ -128,7 +128,7 @@ class PublishingUpdatesGeneratorTest extends TestCase
self::assertEquals(['shortUrl' => [
'shortCode' => $shortUrl->getShortCode(),
'shortUrl' => 'http:/' . $shortUrl->getShortCode(),
'longUrl' => '',
'longUrl' => 'longUrl',
'dateCreated' => $shortUrl->getDateCreated()->toAtomString(),
'visitsCount' => 0,
'tags' => [],

View file

@ -68,7 +68,7 @@ class NotifyNewShortUrlToRabbitMqTest extends TestCase
$shortUrlId = '123';
$update = Update::forTopicAndPayload(Topic::NEW_SHORT_URL->value, []);
$this->em->expects($this->once())->method('find')->with(ShortUrl::class, $shortUrlId)->willReturn(
ShortUrl::withLongUrl(''),
ShortUrl::withLongUrl('longUrl'),
);
$this->updatesGenerator->expects($this->once())->method('newShortUrlUpdate')->with(
$this->isInstanceOf(ShortUrl::class),
@ -88,7 +88,7 @@ class NotifyNewShortUrlToRabbitMqTest extends TestCase
$shortUrlId = '123';
$update = Update::forTopicAndPayload(Topic::NEW_SHORT_URL->value, []);
$this->em->expects($this->once())->method('find')->with(ShortUrl::class, $shortUrlId)->willReturn(
ShortUrl::withLongUrl(''),
ShortUrl::withLongUrl('longUrl'),
);
$this->updatesGenerator->expects($this->once())->method('newShortUrlUpdate')->with(
$this->isInstanceOf(ShortUrl::class),

View file

@ -159,7 +159,7 @@ class NotifyVisitToRabbitMqTest extends TestCase
{
yield 'legacy non-orphan visit' => [
true,
$visit = Visit::forValidShortUrl(ShortUrl::withLongUrl(''), Visitor::emptyInstance()),
$visit = Visit::forValidShortUrl(ShortUrl::withLongUrl('longUrl'), Visitor::emptyInstance()),
noop(...),
function (MockObject & PublishingHelperInterface $helper) use ($visit): void {
$helper->method('publishUpdate')->with($this->callback(function (Update $update) use ($visit): bool {
@ -190,7 +190,7 @@ class NotifyVisitToRabbitMqTest extends TestCase
];
yield 'non-legacy non-orphan visit' => [
false,
Visit::forValidShortUrl(ShortUrl::withLongUrl(''), Visitor::emptyInstance()),
Visit::forValidShortUrl(ShortUrl::withLongUrl('longUrl'), Visitor::emptyInstance()),
function (MockObject & PublishingUpdatesGeneratorInterface $updatesGenerator): void {
$update = Update::forTopicAndPayload('', []);
$updatesGenerator->expects($this->never())->method('newOrphanVisitUpdate');

View file

@ -55,7 +55,7 @@ class NotifyNewShortUrlToRedisTest extends TestCase
$shortUrlId = '123';
$update = Update::forTopicAndPayload(Topic::NEW_SHORT_URL->value, []);
$this->em->expects($this->once())->method('find')->with(ShortUrl::class, $shortUrlId)->willReturn(
ShortUrl::withLongUrl(''),
ShortUrl::withLongUrl('longUrl'),
);
$this->updatesGenerator->expects($this->once())->method('newShortUrlUpdate')->with(
$this->isInstanceOf(ShortUrl::class),

View file

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\Core\Functions;
use BackedEnum;
use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\Core\Config\EnvVars;
use Shlinkio\Shlink\Core\Model\DeviceType;
@ -16,6 +17,7 @@ use function Shlinkio\Shlink\Core\enumValues;
class FunctionsTest extends TestCase
{
/**
* @param class-string<BackedEnum> $enum
* @test
* @dataProvider provideEnums
*/

View file

@ -38,7 +38,7 @@ class ShortUrlTest extends TestCase
public function provideInvalidShortUrls(): iterable
{
yield 'with custom slug' => [
ShortUrl::create(ShortUrlCreation::fromRawData(['customSlug' => 'custom-slug', 'longUrl' => ''])),
ShortUrl::create(ShortUrlCreation::fromRawData(['customSlug' => 'custom-slug', 'longUrl' => 'longUrl'])),
'The short code cannot be regenerated on ShortUrls where a custom slug was provided.',
];
yield 'already persisted' => [
@ -66,7 +66,7 @@ class ShortUrlTest extends TestCase
{
yield 'no custom slug' => [ShortUrl::createEmpty()];
yield 'imported with custom slug' => [ShortUrl::fromImport(
new ImportedShlinkUrl(ImportSource::BITLY, '', [], Chronos::now(), null, 'custom-slug', null),
new ImportedShlinkUrl(ImportSource::BITLY, 'longUrl', [], Chronos::now(), null, 'custom-slug', null),
true,
)];
}
@ -78,7 +78,7 @@ class ShortUrlTest extends TestCase
public function shortCodesHaveExpectedLength(?int $length, int $expectedLength): void
{
$shortUrl = ShortUrl::create(ShortUrlCreation::fromRawData(
[ShortUrlInputFilter::SHORT_CODE_LENGTH => $length, 'longUrl' => ''],
[ShortUrlInputFilter::SHORT_CODE_LENGTH => $length, 'longUrl' => 'longUrl'],
));
self::assertEquals($expectedLength, strlen($shortUrl->getShortCode()));

View file

@ -30,7 +30,7 @@ class ShortUrlStringifierTest extends TestCase
{
$shortUrlWithShortCode = fn (string $shortCode, ?string $domain = null) => ShortUrl::create(
ShortUrlCreation::fromRawData([
'longUrl' => '',
'longUrl' => 'longUrl',
'customSlug' => $shortCode,
'domain' => $domain,
]),

View file

@ -142,7 +142,7 @@ class ExtraPathRedirectMiddlewareTest extends TestCase
$type->method('isInvalidShortUrl')->willReturn(true);
$request = ServerRequestFactory::fromGlobals()->withAttribute(NotFoundType::class, $type)
->withUri(new Uri('https://s.test/shortCode/bar/baz'));
$shortUrl = ShortUrl::withLongUrl('');
$shortUrl = ShortUrl::withLongUrl('longUrl');
$currentIteration = 1;
$this->resolver->expects($this->exactly($expectedResolveCalls))->method('resolveEnabledShortUrl')->with(

View file

@ -80,24 +80,24 @@ class ShortUrlCreationTest extends TestCase
string $expectedSlug,
bool $multiSegmentEnabled = false,
): void {
$meta = ShortUrlCreation::fromRawData([
$creation = ShortUrlCreation::fromRawData([
'validSince' => Chronos::parse('2015-01-01')->toAtomString(),
'customSlug' => $customSlug,
'longUrl' => '',
'longUrl' => 'longUrl',
EnvVars::MULTI_SEGMENT_SLUGS_ENABLED->value => $multiSegmentEnabled,
]);
self::assertTrue($meta->hasValidSince());
self::assertEquals(Chronos::parse('2015-01-01'), $meta->getValidSince());
self::assertTrue($creation->hasValidSince());
self::assertEquals(Chronos::parse('2015-01-01'), $creation->validSince);
self::assertFalse($meta->hasValidUntil());
self::assertNull($meta->getValidUntil());
self::assertFalse($creation->hasValidUntil());
self::assertNull($creation->validUntil);
self::assertTrue($meta->hasCustomSlug());
self::assertEquals($expectedSlug, $meta->getCustomSlug());
self::assertTrue($creation->hasCustomSlug());
self::assertEquals($expectedSlug, $creation->customSlug);
self::assertFalse($meta->hasMaxVisits());
self::assertNull($meta->getMaxVisits());
self::assertFalse($creation->hasMaxVisits());
self::assertNull($creation->maxVisits);
}
public function provideCustomSlugs(): iterable
@ -127,12 +127,12 @@ class ShortUrlCreationTest extends TestCase
*/
public function titleIsCroppedIfTooLong(?string $title, ?string $expectedTitle): void
{
$meta = ShortUrlCreation::fromRawData([
$creation = ShortUrlCreation::fromRawData([
'title' => $title,
'longUrl' => '',
'longUrl' => 'longUrl',
]);
self::assertEquals($expectedTitle, $meta->getTitle());
self::assertEquals($expectedTitle, $creation->title);
}
public function provideTitles(): iterable
@ -153,12 +153,12 @@ class ShortUrlCreationTest extends TestCase
*/
public function emptyDomainIsDiscarded(?string $domain, ?string $expectedDomain): void
{
$meta = ShortUrlCreation::fromRawData([
$creation = ShortUrlCreation::fromRawData([
'domain' => $domain,
'longUrl' => '',
'longUrl' => 'longUrl',
]);
self::assertSame($expectedDomain, $meta->getDomain());
self::assertSame($expectedDomain, $creation->domain);
}
public function provideDomains(): iterable

View file

@ -114,7 +114,7 @@ class ShortUrlResolverTest extends TestCase
$now = Chronos::now();
yield 'maxVisits reached' => [(function () {
$shortUrl = ShortUrl::create(ShortUrlCreation::fromRawData(['maxVisits' => 3, 'longUrl' => '']));
$shortUrl = ShortUrl::create(ShortUrlCreation::fromRawData(['maxVisits' => 3, 'longUrl' => 'longUrl']));
$shortUrl->setVisits(new ArrayCollection(map(
range(0, 4),
fn () => Visit::forValidShortUrl($shortUrl, Visitor::emptyInstance()),
@ -123,16 +123,16 @@ class ShortUrlResolverTest extends TestCase
return $shortUrl;
})()];
yield 'future validSince' => [ShortUrl::create(ShortUrlCreation::fromRawData(
['validSince' => $now->addMonth()->toAtomString(), 'longUrl' => ''],
['validSince' => $now->addMonth()->toAtomString(), 'longUrl' => 'longUrl'],
))];
yield 'past validUntil' => [ShortUrl::create(ShortUrlCreation::fromRawData(
['validUntil' => $now->subMonth()->toAtomString(), 'longUrl' => ''],
['validUntil' => $now->subMonth()->toAtomString(), 'longUrl' => 'longUrl'],
))];
yield 'mixed' => [(function () use ($now) {
$shortUrl = ShortUrl::create(ShortUrlCreation::fromRawData([
'maxVisits' => 3,
'validUntil' => $now->subMonth()->toAtomString(),
'longUrl' => '',
'longUrl' => 'longUrl',
]));
$shortUrl->setVisits(new ArrayCollection(map(
range(0, 4),

View file

@ -45,7 +45,7 @@ class ShortUrlDataTransformerTest extends TestCase
]];
yield 'max visits only' => [ShortUrl::create(ShortUrlCreation::fromRawData([
'maxVisits' => $maxVisits,
'longUrl' => '',
'longUrl' => 'longUrl',
])), [
'validSince' => null,
'validUntil' => null,
@ -53,7 +53,7 @@ class ShortUrlDataTransformerTest extends TestCase
]];
yield 'max visits and valid since' => [
ShortUrl::create(ShortUrlCreation::fromRawData(
['validSince' => $now, 'maxVisits' => $maxVisits, 'longUrl' => ''],
['validSince' => $now, 'maxVisits' => $maxVisits, 'longUrl' => 'longUrl'],
)),
[
'validSince' => $now->toAtomString(),
@ -63,7 +63,7 @@ class ShortUrlDataTransformerTest extends TestCase
];
yield 'both dates' => [
ShortUrl::create(ShortUrlCreation::fromRawData(
['validSince' => $now, 'validUntil' => $now->subDays(10), 'longUrl' => ''],
['validSince' => $now, 'validUntil' => $now->subDays(10), 'longUrl' => 'longUrl'],
)),
[
'validSince' => $now->toAtomString(),
@ -72,9 +72,12 @@ class ShortUrlDataTransformerTest extends TestCase
],
];
yield 'everything' => [
ShortUrl::create(ShortUrlCreation::fromRawData(
['validSince' => $now, 'validUntil' => $now->subDays(5), 'maxVisits' => $maxVisits, 'longUrl' => ''],
)),
ShortUrl::create(ShortUrlCreation::fromRawData([
'validSince' => $now,
'validUntil' => $now->subDays(5),
'maxVisits' => $maxVisits,
'longUrl' => 'longUrl',
])),
[
'validSince' => $now->toAtomString(),
'validUntil' => $now->subDays(5)->toAtomString(),

View file

@ -261,9 +261,8 @@ class CreateShortUrlTest extends ApiTestCase
public function provideInvalidUrls(): iterable
{
yield 'empty URL' => ['', '2', 'INVALID_URL'];
yield 'non-reachable URL' => ['https://this-has-to-be-invalid.com', '2', 'INVALID_URL'];
yield 'API version 3' => ['', '3', 'https://shlink.io/api/error/invalid-url'];
yield 'API version 2' => ['https://this-has-to-be-invalid.com', '2', 'INVALID_URL'];
yield 'API version 3' => ['https://this-has-to-be-invalid.com', '3', 'https://shlink.io/api/error/invalid-url'];
}
/**