diff --git a/module/CLI/test/Command/ShortUrl/CreateShortUrlCommandTest.php b/module/CLI/test/Command/ShortUrl/CreateShortUrlCommandTest.php index 734089c9..1a8df888 100644 --- a/module/CLI/test/Command/ShortUrl/CreateShortUrlCommandTest.php +++ b/module/CLI/test/Command/ShortUrl/CreateShortUrlCommandTest.php @@ -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()); diff --git a/module/Core/src/ShortUrl/Entity/DeviceLongUrl.php b/module/Core/src/ShortUrl/Entity/DeviceLongUrl.php index faf9bcc3..b1dc1086 100644 --- a/module/Core/src/ShortUrl/Entity/DeviceLongUrl.php +++ b/module/Core/src/ShortUrl/Entity/DeviceLongUrl.php @@ -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; + } } diff --git a/module/Core/src/ShortUrl/Entity/ShortUrl.php b/module/Core/src/ShortUrl/Entity/ShortUrl.php index 0ebdeb24..6c49e1c3 100644 --- a/module/Core/src/ShortUrl/Entity/ShortUrl.php +++ b/module/Core/src/ShortUrl/Entity/ShortUrl.php @@ -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; } diff --git a/module/Core/src/ShortUrl/Model/ShortUrlCreation.php b/module/Core/src/ShortUrl/Model/ShortUrlCreation.php index bbdd9ab0..e2af5cf1 100644 --- a/module/Core/src/ShortUrl/Model/ShortUrlCreation.php +++ b/module/Core/src/ShortUrl/Model/ShortUrlCreation.php @@ -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; - } } diff --git a/module/Core/src/ShortUrl/Model/Validation/ShortUrlInputFilter.php b/module/Core/src/ShortUrl/Model/Validation/ShortUrlInputFilter.php index a6c5627f..f31ee294 100644 --- a/module/Core/src/ShortUrl/Model/Validation/ShortUrlInputFilter.php +++ b/module/Core/src/ShortUrl/Model/Validation/ShortUrlInputFilter.php @@ -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)); diff --git a/module/Core/src/ShortUrl/Repository/ShortUrlRepository.php b/module/Core/src/ShortUrl/Repository/ShortUrlRepository.php index 5e95f777..ee2f7389 100644 --- a/module/Core/src/ShortUrl/Repository/ShortUrlRepository.php +++ b/module/Core/src/ShortUrl/Repository/ShortUrlRepository.php @@ -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(); diff --git a/module/Core/src/ShortUrl/Repository/ShortUrlRepositoryInterface.php b/module/Core/src/ShortUrl/Repository/ShortUrlRepositoryInterface.php index cc574ac5..18a4ec71 100644 --- a/module/Core/src/ShortUrl/Repository/ShortUrlRepositoryInterface.php +++ b/module/Core/src/ShortUrl/Repository/ShortUrlRepositoryInterface.php @@ -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; } diff --git a/module/Core/src/ShortUrl/UrlShortener.php b/module/Core/src/ShortUrl/UrlShortener.php index d3f54650..4236189c 100644 --- a/module/Core/src/ShortUrl/UrlShortener.php +++ b/module/Core/src/ShortUrl/UrlShortener.php @@ -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 diff --git a/module/Core/test-db/Tag/Repository/TagRepositoryTest.php b/module/Core/test-db/Tag/Repository/TagRepositoryTest.php index 57b3a795..4365731a 100644 --- a/module/Core/test-db/Tag/Repository/TagRepositoryTest.php +++ b/module/Core/test-db/Tag/Repository/TagRepositoryTest.php @@ -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, ); diff --git a/module/Core/test-db/Visit/Repository/VisitRepositoryTest.php b/module/Core/test-db/Visit/Repository/VisitRepositoryTest.php index 2e509aa2..df4c5334 100644 --- a/module/Core/test-db/Visit/Repository/VisitRepositoryTest.php +++ b/module/Core/test-db/Visit/Repository/VisitRepositoryTest.php @@ -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); diff --git a/module/Core/test/EventDispatcher/Mercure/NotifyNewShortUrlToMercureTest.php b/module/Core/test/EventDispatcher/Mercure/NotifyNewShortUrlToMercureTest.php index c42bd915..855f6c14 100644 --- a/module/Core/test/EventDispatcher/Mercure/NotifyNewShortUrlToMercureTest.php +++ b/module/Core/test/EventDispatcher/Mercure/NotifyNewShortUrlToMercureTest.php @@ -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'); diff --git a/module/Core/test/EventDispatcher/PublishingUpdatesGeneratorTest.php b/module/Core/test/EventDispatcher/PublishingUpdatesGeneratorTest.php index cda8fe98..9611df99 100644 --- a/module/Core/test/EventDispatcher/PublishingUpdatesGeneratorTest.php +++ b/module/Core/test/EventDispatcher/PublishingUpdatesGeneratorTest.php @@ -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' => [], diff --git a/module/Core/test/EventDispatcher/RabbitMq/NotifyNewShortUrlToRabbitMqTest.php b/module/Core/test/EventDispatcher/RabbitMq/NotifyNewShortUrlToRabbitMqTest.php index 764f7949..52e9630d 100644 --- a/module/Core/test/EventDispatcher/RabbitMq/NotifyNewShortUrlToRabbitMqTest.php +++ b/module/Core/test/EventDispatcher/RabbitMq/NotifyNewShortUrlToRabbitMqTest.php @@ -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), diff --git a/module/Core/test/EventDispatcher/RabbitMq/NotifyVisitToRabbitMqTest.php b/module/Core/test/EventDispatcher/RabbitMq/NotifyVisitToRabbitMqTest.php index 8b7b392c..6211ad2b 100644 --- a/module/Core/test/EventDispatcher/RabbitMq/NotifyVisitToRabbitMqTest.php +++ b/module/Core/test/EventDispatcher/RabbitMq/NotifyVisitToRabbitMqTest.php @@ -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'); diff --git a/module/Core/test/EventDispatcher/RedisPubSub/NotifyNewShortUrlToRedisTest.php b/module/Core/test/EventDispatcher/RedisPubSub/NotifyNewShortUrlToRedisTest.php index 0b5dfd27..a913de15 100644 --- a/module/Core/test/EventDispatcher/RedisPubSub/NotifyNewShortUrlToRedisTest.php +++ b/module/Core/test/EventDispatcher/RedisPubSub/NotifyNewShortUrlToRedisTest.php @@ -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), diff --git a/module/Core/test/Functions/FunctionsTest.php b/module/Core/test/Functions/FunctionsTest.php index 5ba6a7db..ad45812f 100644 --- a/module/Core/test/Functions/FunctionsTest.php +++ b/module/Core/test/Functions/FunctionsTest.php @@ -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 $enum * @test * @dataProvider provideEnums */ diff --git a/module/Core/test/ShortUrl/Entity/ShortUrlTest.php b/module/Core/test/ShortUrl/Entity/ShortUrlTest.php index 026778ae..fd4515fb 100644 --- a/module/Core/test/ShortUrl/Entity/ShortUrlTest.php +++ b/module/Core/test/ShortUrl/Entity/ShortUrlTest.php @@ -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())); diff --git a/module/Core/test/ShortUrl/Helper/ShortUrlStringifierTest.php b/module/Core/test/ShortUrl/Helper/ShortUrlStringifierTest.php index b6d5a123..fc8c7579 100644 --- a/module/Core/test/ShortUrl/Helper/ShortUrlStringifierTest.php +++ b/module/Core/test/ShortUrl/Helper/ShortUrlStringifierTest.php @@ -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, ]), diff --git a/module/Core/test/ShortUrl/Middleware/ExtraPathRedirectMiddlewareTest.php b/module/Core/test/ShortUrl/Middleware/ExtraPathRedirectMiddlewareTest.php index 835d1487..696a47ab 100644 --- a/module/Core/test/ShortUrl/Middleware/ExtraPathRedirectMiddlewareTest.php +++ b/module/Core/test/ShortUrl/Middleware/ExtraPathRedirectMiddlewareTest.php @@ -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( diff --git a/module/Core/test/ShortUrl/Model/ShortUrlCreationTest.php b/module/Core/test/ShortUrl/Model/ShortUrlCreationTest.php index ee9e540a..33380ecf 100644 --- a/module/Core/test/ShortUrl/Model/ShortUrlCreationTest.php +++ b/module/Core/test/ShortUrl/Model/ShortUrlCreationTest.php @@ -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 diff --git a/module/Core/test/ShortUrl/ShortUrlResolverTest.php b/module/Core/test/ShortUrl/ShortUrlResolverTest.php index 9c2bcab3..177e432e 100644 --- a/module/Core/test/ShortUrl/ShortUrlResolverTest.php +++ b/module/Core/test/ShortUrl/ShortUrlResolverTest.php @@ -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), diff --git a/module/Core/test/ShortUrl/Transformer/ShortUrlDataTransformerTest.php b/module/Core/test/ShortUrl/Transformer/ShortUrlDataTransformerTest.php index c9df4e38..7a97d4da 100644 --- a/module/Core/test/ShortUrl/Transformer/ShortUrlDataTransformerTest.php +++ b/module/Core/test/ShortUrl/Transformer/ShortUrlDataTransformerTest.php @@ -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(), diff --git a/module/Rest/test-api/Action/CreateShortUrlTest.php b/module/Rest/test-api/Action/CreateShortUrlTest.php index 5190000e..19bd6c74 100644 --- a/module/Rest/test-api/Action/CreateShortUrlTest.php +++ b/module/Rest/test-api/Action/CreateShortUrlTest.php @@ -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']; } /**