From 8c3240c7bf63c4daff3732fa01db9234d15c146d Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Thu, 30 Apr 2026 22:17:38 +0200 Subject: [PATCH] Use valinor to validate and map short URL creation --- composer.json | 2 +- config/constants.php | 4 +- .../ShortUrl/Input/ShortUrlCreationInput.php | 21 +- .../ShortUrl/ListShortUrlsCommandTest.php | 10 +- module/Core/src/ShortUrl/Entity/ShortUrl.php | 30 +-- .../src/ShortUrl/Model/ShortUrlCreation.php | 103 +++++--- .../src/ShortUrl/Model/ShortUrlsParams.php | 3 + .../Model/Validation/CustomSlugFilter.php | 1 + .../Model/Validation/CustomSlugValidator.php | 1 + .../Model/Validation/ShortUrlInputFilter.php | 52 +--- .../Repository/DomainRepositoryTest.php | 6 +- .../CrawlableShortCodesQueryTest.php | 2 +- .../DeleteExpiredShortUrlsRepositoryTest.php | 18 +- .../Repository/ShortUrlListRepositoryTest.php | 107 +++----- .../Repository/ShortUrlRepositoryTest.php | 248 +++++++++--------- .../Tag/Repository/TagRepositoryTest.php | 16 +- .../Repository/VisitDeleterRepositoryTest.php | 19 +- .../Visit/Repository/VisitRepositoryTest.php | 55 ++-- .../PublishingUpdatesGeneratorTest.php | 20 +- .../RabbitMq/NotifyVisitToRabbitMqTest.php | 5 +- .../test/Matomo/MatomoVisitSenderTest.php | 6 +- .../ShortUrlRedirectionResolverTest.php | 5 +- .../test/ShortUrl/Entity/ShortUrlTest.php | 30 +-- .../Helper/ShortUrlRedirectionBuilderTest.php | 8 +- .../Helper/ShortUrlStringifierTest.php | 6 +- .../ShortUrlTitleResolutionHelperTest.php | 21 +- .../ShortUrl/Model/ShortUrlCreationTest.php | 147 +---------- .../test/ShortUrl/ShortUrlResolverTest.php | 24 +- .../ShortUrlDataTransformerTest.php | 36 ++- .../Core/test/ShortUrl/UrlShortenerTest.php | 78 +++--- module/Rest/config/dependencies.config.php | 2 + .../ShortUrl/AbstractCreateShortUrlAction.php | 13 +- .../Action/ShortUrl/CreateShortUrlAction.php | 13 +- .../SingleStepCreateShortUrlAction.php | 12 +- .../ShortUrl/OverrideDomainMiddleware.php | 14 +- .../test-api/Action/CreateShortUrlTest.php | 73 +++++- .../test-api/Fixtures/ShortUrlsFixture.php | 76 +++--- .../ShortUrl/CreateShortUrlActionTest.php | 64 ++--- .../SingleStepCreateShortUrlActionTest.php | 6 +- 39 files changed, 591 insertions(+), 766 deletions(-) diff --git a/composer.json b/composer.json index d9c41241..d170393e 100644 --- a/composer.json +++ b/composer.json @@ -42,7 +42,7 @@ "pagerfanta/core": "^3.8", "ramsey/uuid": "^4.7", "shlinkio/doctrine-specification": "^2.2", - "shlinkio/shlink-common": "dev-main#6b13d94 as 8.2.0", + "shlinkio/shlink-common": "dev-main#ced752b as 8.2.0", "shlinkio/shlink-config": "^4.1.0", "shlinkio/shlink-event-dispatcher": "^4.4.0", "shlinkio/shlink-importer": "^5.7.0", diff --git a/config/constants.php b/config/constants.php index 6ed765e3..05fa78ba 100644 --- a/config/constants.php +++ b/config/constants.php @@ -13,10 +13,12 @@ const DEFAULT_REDIRECT_STATUS_CODE = RedirectStatus::STATUS_302; const DEFAULT_REDIRECT_CACHE_LIFETIME = 30; const DEFAULT_REDIRECT_CACHE_VISIBILITY = 'private'; const LOCAL_LOCK_FACTORY = 'Shlinkio\Shlink\LocalLockFactory'; -const LOOSE_URI_MATCHER = '/(.+)\:(.+)/i'; // Matches anything starting with a schema. const IP_ADDRESS_REQUEST_ATTRIBUTE = 'remote_address'; const REDIRECT_URL_REQUEST_ATTRIBUTE = 'redirect_url'; +/** @deprecated */ +const LOOSE_URI_MATCHER = '/(.+)\:(.+)/i'; // Matches anything starting with a schema. + /** * List of ISO 3166-1 alpha-2 two-letter country codes https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2 */ diff --git a/module/CLI/src/Command/ShortUrl/Input/ShortUrlCreationInput.php b/module/CLI/src/Command/ShortUrl/Input/ShortUrlCreationInput.php index 729c8a94..193a03d1 100644 --- a/module/CLI/src/Command/ShortUrl/Input/ShortUrlCreationInput.php +++ b/module/CLI/src/Command/ShortUrl/Input/ShortUrlCreationInput.php @@ -6,12 +6,13 @@ namespace Shlinkio\Shlink\CLI\Command\ShortUrl\Input; use Shlinkio\Shlink\Core\Config\Options\UrlShortenerOptions; use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlCreation; -use Shlinkio\Shlink\Core\ShortUrl\Model\Validation\ShortUrlInputFilter; use Symfony\Component\Console\Attribute\Argument; use Symfony\Component\Console\Attribute\Ask; use Symfony\Component\Console\Attribute\MapInput; use Symfony\Component\Console\Attribute\Option; +use function max; + /** * Data used for short URL creation */ @@ -43,15 +44,15 @@ final class ShortUrlCreationInput public function toShortUrlCreation(UrlShortenerOptions $options): ShortUrlCreation { - $shortCodeLength = $this->shortCodeLength ?? $options->defaultShortCodesLength; - return ShortUrlCreation::fromRawData([ - ShortUrlInputFilter::LONG_URL => $this->longUrl, - ShortUrlInputFilter::DOMAIN => $this->domain, - ShortUrlInputFilter::CUSTOM_SLUG => $this->customSlug, - ShortUrlInputFilter::SHORT_CODE_LENGTH => $shortCodeLength, - ShortUrlInputFilter::PATH_PREFIX => $this->pathPrefix, - ShortUrlInputFilter::FIND_IF_EXISTS => $this->findIfExists, + $shortCodeLength = max(4, $this->shortCodeLength ?? $options->defaultShortCodesLength); + return new ShortUrlCreation( + $this->longUrl, ...$this->commonData->toArray(), - ], $options); + customSlug: $this->customSlug, + pathPrefix: $this->pathPrefix, + findIfExists: $this->findIfExists, + domain: $this->domain, + shortCodeLength: $shortCodeLength, + ); } } diff --git a/module/CLI/test/Command/ShortUrl/ListShortUrlsCommandTest.php b/module/CLI/test/Command/ShortUrl/ListShortUrlsCommandTest.php index 3c61ea6b..7a641767 100644 --- a/module/CLI/test/Command/ShortUrl/ListShortUrlsCommandTest.php +++ b/module/CLI/test/Command/ShortUrl/ListShortUrlsCommandTest.php @@ -121,11 +121,11 @@ class ListShortUrlsCommandTest extends TestCase public static function provideOptionalFlags(): iterable { - $shortUrl = ShortUrl::create(ShortUrlCreation::fromRawData([ - 'longUrl' => 'https://foo.com', - 'tags' => ['foo', 'bar', 'baz'], - 'apiKey' => ApiKey::fromMeta(ApiKeyMeta::fromParams(name: 'my api key')), - ])); + $shortUrl = ShortUrl::create(new ShortUrlCreation( + longUrl: 'https://foo.com', + apiKey: ApiKey::fromMeta(ApiKeyMeta::fromParams(name: 'my api key')), + tags: ['foo', 'bar', 'baz'], + )); $shortCode = $shortUrl->getShortCode(); $created = $shortUrl->dateCreated()->toAtomString(); diff --git a/module/Core/src/ShortUrl/Entity/ShortUrl.php b/module/Core/src/ShortUrl/Entity/ShortUrl.php index 5febe0ae..5c8ec7a2 100644 --- a/module/Core/src/ShortUrl/Entity/ShortUrl.php +++ b/module/Core/src/ShortUrl/Entity/ShortUrl.php @@ -16,7 +16,6 @@ use Shlinkio\Shlink\Core\RedirectRule\Entity\ShortUrlRedirectRule; 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\Core\ShortUrl\Resolver\ShortUrlRelationResolverInterface; use Shlinkio\Shlink\Core\ShortUrl\Resolver\SimpleShortUrlRelationResolver; use Shlinkio\Shlink\Core\Tag\Entity\Tag; @@ -30,7 +29,6 @@ use Shlinkio\Shlink\Rest\Entity\ApiKey; use function array_map; use function count; use function Shlinkio\Shlink\Common\normalizeDate; -use function Shlinkio\Shlink\Common\normalizeOptionalDate; use function Shlinkio\Shlink\Core\generateRandomShortCode; use function sprintf; @@ -79,7 +77,7 @@ class ShortUrl extends AbstractEntity */ public static function withLongUrl(string $longUrl): self { - return self::create(ShortUrlCreation::fromRawData([ShortUrlInputFilter::LONG_URL => $longUrl])); + return self::create(new ShortUrlCreation(longUrl: $longUrl)); } public static function create( @@ -94,6 +92,8 @@ class ShortUrl extends AbstractEntity shortCode: sprintf( '%s%s', $creation->pathPrefix ?? '', + // TODO Encapsulate Generating the random short code into ShortUrlCreation, when custom slug is not set, + // then expose it as shortCode or something generic $creation->customSlug ?? generateRandomShortCode($shortCodeLength, $creation->shortUrlMode), ), tags: $relationResolver->resolveTags($creation->tags), @@ -116,21 +116,17 @@ class ShortUrl extends AbstractEntity bool $importShortCode, ShortUrlRelationResolverInterface|null $relationResolver = null, ): self { - $meta = [ - ShortUrlInputFilter::LONG_URL => $url->longUrl, - ShortUrlInputFilter::DOMAIN => $url->domain, - ShortUrlInputFilter::TAGS => $url->tags, - ShortUrlInputFilter::TITLE => $url->title, - ShortUrlInputFilter::MAX_VISITS => $url->meta->maxVisits, - ]; - if ($importShortCode) { - $meta[ShortUrlInputFilter::CUSTOM_SLUG] = $url->shortCode; - } + $instance = self::create(new ShortUrlCreation( + longUrl: $url->longUrl, + validSince: $url->meta->validSince, + validUntil: $url->meta->validUntil, + customSlug: $importShortCode ? $url->shortCode : null, + maxVisits: $url->meta->maxVisits, + domain: $url->domain, + tags: $url->tags, + title: $url->title, + ), $relationResolver); - $instance = self::create(ShortUrlCreation::fromRawData($meta), $relationResolver); - - $instance->validSince = normalizeOptionalDate($url->meta->validSince); - $instance->validUntil = normalizeOptionalDate($url->meta->validUntil); $instance->dateCreated = normalizeDate($url->createdAt); $instance->importSource = $url->source->value; $instance->importOriginalShortCode = $url->shortCode; diff --git a/module/Core/src/ShortUrl/Model/ShortUrlCreation.php b/module/Core/src/ShortUrl/Model/ShortUrlCreation.php index 6bb5b76f..a3860996 100644 --- a/module/Core/src/ShortUrl/Model/ShortUrlCreation.php +++ b/module/Core/src/ShortUrl/Model/ShortUrlCreation.php @@ -5,83 +5,104 @@ declare(strict_types=1); namespace Shlinkio\Shlink\Core\ShortUrl\Model; use Cake\Chronos\Chronos; -use Shlinkio\Shlink\Core\Config\Options\UrlShortenerOptions; -use Shlinkio\Shlink\Core\Exception\ValidationException; +use DateTimeInterface; +use Shlinkio\Shlink\Common\ObjectMapper\HostAndPortConverter; +use Shlinkio\Shlink\Common\ObjectMapper\LooseUriConverter; +use Shlinkio\Shlink\Common\ObjectMapper\MappingError; +use Shlinkio\Shlink\Common\ObjectMapper\SubstringConverter; +use Shlinkio\Shlink\Common\ObjectMapper\TagsConverter; use Shlinkio\Shlink\Core\ShortUrl\Helper\TitleResolutionModelInterface; -use Shlinkio\Shlink\Core\ShortUrl\Model\Validation\ShortUrlInputFilter; use Shlinkio\Shlink\Rest\Entity\ApiKey; use function Shlinkio\Shlink\Common\normalizeOptionalDate; -use function Shlinkio\Shlink\Core\getNonEmptyOptionalValueFromInputFilter; -use function Shlinkio\Shlink\Core\getOptionalBoolFromInputFilter; -use function Shlinkio\Shlink\Core\getOptionalIntFromInputFilter; +use function str_replace; +use function strpbrk; +use function strtolower; +use function trim; use const Shlinkio\Shlink\DEFAULT_SHORT_CODES_LENGTH; final readonly class ShortUrlCreation implements TitleResolutionModelInterface { + public Chronos|null $validSince; + public Chronos|null $validUntil; + public string|null $customSlug; + public string|null $pathPrefix; + /** * @param string[] $tags + * @param int<4, max> $shortCodeLength */ - private function __construct( + public function __construct( + #[LooseUriConverter] public string $longUrl, - public ShortUrlMode $shortUrlMode, - public Chronos|null $validSince = null, - public Chronos|null $validUntil = null, - public string|null $customSlug = null, - public string|null $pathPrefix = null, + DateTimeInterface|null $validSince = null, + DateTimeInterface|null $validUntil = null, + public ShortUrlMode $shortUrlMode = ShortUrlMode::STRICT, + private bool $multiSegmentSlugsEnabled = false, + string|null $customSlug = null, + string|null $pathPrefix = null, public int|null $maxVisits = null, public bool $findIfExists = false, + #[HostAndPortConverter] public string|null $domain = null, - public int $shortCodeLength = 5, + public int $shortCodeLength = DEFAULT_SHORT_CODES_LENGTH, public ApiKey|null $apiKey = null, + #[TagsConverter] public array $tags = [], + #[SubstringConverter(512)] public string|null $title = null, public bool $titleWasAutoResolved = false, public bool $crawlable = false, public bool $forwardQuery = true, ) { + $this->validSince = normalizeOptionalDate($validSince); + $this->validUntil = normalizeOptionalDate($validUntil); + + $this->customSlug = $this->filterAndValidateOptionsRelatedValue($customSlug); + $this->pathPrefix = $this->filterAndValidateOptionsRelatedValue($pathPrefix); } - /** - * @throws ValidationException - */ - public static function fromRawData(array $data, UrlShortenerOptions $options = new UrlShortenerOptions()): self + private function filterAndValidateOptionsRelatedValue(string|null $value): string|null { - $inputFilter = ShortUrlInputFilter::forCreation($data, $options); - if (! $inputFilter->isValid()) { - throw ValidationException::fromInputFilter($inputFilter); + if ($value === null) { + return null; } - return new self( - longUrl: $inputFilter->getValue(ShortUrlInputFilter::LONG_URL), - shortUrlMode: $options->mode, - validSince: normalizeOptionalDate($inputFilter->getValue(ShortUrlInputFilter::VALID_SINCE)), - validUntil: normalizeOptionalDate($inputFilter->getValue(ShortUrlInputFilter::VALID_UNTIL)), - customSlug: $inputFilter->getValue(ShortUrlInputFilter::CUSTOM_SLUG), - pathPrefix: $inputFilter->getValue(ShortUrlInputFilter::PATH_PREFIX), - 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, - 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, - ); + $isLooseMode = $this->shortUrlMode === ShortUrlMode::LOOSE; + $value = $isLooseMode ? strtolower($value) : $value; + $value = $this->multiSegmentSlugsEnabled + ? trim(str_replace(' ', '-', $value), '/') + : str_replace([' ', '/'], '-', $value); + + // URL gen-delimiter reserved characters, except `/`: https://datatracker.ietf.org/doc/html/rfc3986#section-2.2 + $reservedChars = ':?#[]@'; + if (! $this->multiSegmentSlugsEnabled) { + // Slashes should only be allowed if multi-segment slugs are enabled + $reservedChars .= '/'; + } + + if (strpbrk($value, $reservedChars) !== false) { + throw MappingError::withBody('URL-reserved characters cannot be used in a custom slug or path prefix'); + } + + return $value; } public function withResolvedTitle(string $title): static { + // TODO Use clone with once PHP 8.4 is no longer supported + // return clone($this, [ + // 'title' => $title, + // 'titleWasAutoResolved' => true, + // ]); + return new self( longUrl: $this->longUrl, - shortUrlMode: $this->shortUrlMode, validSince: $this->validSince, validUntil: $this->validUntil, + shortUrlMode: $this->shortUrlMode, + multiSegmentSlugsEnabled: $this->multiSegmentSlugsEnabled, customSlug: $this->customSlug, pathPrefix: $this->pathPrefix, maxVisits: $this->maxVisits, diff --git a/module/Core/src/ShortUrl/Model/ShortUrlsParams.php b/module/Core/src/ShortUrl/Model/ShortUrlsParams.php index ffe63b4a..e9ca214f 100644 --- a/module/Core/src/ShortUrl/Model/ShortUrlsParams.php +++ b/module/Core/src/ShortUrl/Model/ShortUrlsParams.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace Shlinkio\Shlink\Core\ShortUrl\Model; use DateTimeInterface; +use Shlinkio\Shlink\Common\ObjectMapper\TagsConverter; use Shlinkio\Shlink\Common\Util\DateRange; use Shlinkio\Shlink\Core\Model\Ordering; use Shlinkio\Shlink\Core\ObjectMapper\OrderingConverter; @@ -34,6 +35,7 @@ final readonly class ShortUrlsParams public int $page = 1, public int $itemsPerPage = self::DEFAULT_ITEMS_PER_PAGE, public string|null $searchTerm = null, + #[TagsConverter] array $tags = [], #[OrderingConverter] public Ordering $orderBy = new Ordering(), @@ -43,6 +45,7 @@ final readonly class ShortUrlsParams public bool $excludePastValidUntil = false, public TagsMode $tagsMode = TagsMode::ANY, public string|null $domain = null, + #[TagsConverter] array $excludeTags = [], public TagsMode $excludeTagsMode = TagsMode::ANY, public string|null $apiKeyName = null, diff --git a/module/Core/src/ShortUrl/Model/Validation/CustomSlugFilter.php b/module/Core/src/ShortUrl/Model/Validation/CustomSlugFilter.php index ba4e3959..2a92606e 100644 --- a/module/Core/src/ShortUrl/Model/Validation/CustomSlugFilter.php +++ b/module/Core/src/ShortUrl/Model/Validation/CustomSlugFilter.php @@ -12,6 +12,7 @@ use function str_replace; use function strtolower; use function trim; +/** @deprecated */ readonly class CustomSlugFilter implements FilterInterface { public function __construct(private UrlShortenerOptions $options) diff --git a/module/Core/src/ShortUrl/Model/Validation/CustomSlugValidator.php b/module/Core/src/ShortUrl/Model/Validation/CustomSlugValidator.php index 3d3e7792..211ff610 100644 --- a/module/Core/src/ShortUrl/Model/Validation/CustomSlugValidator.php +++ b/module/Core/src/ShortUrl/Model/Validation/CustomSlugValidator.php @@ -10,6 +10,7 @@ use Shlinkio\Shlink\Core\Config\Options\UrlShortenerOptions; use function is_string; use function strpbrk; +/** @deprecated */ class CustomSlugValidator extends AbstractValidator { private const string NOT_STRING = 'NOT_STRING'; diff --git a/module/Core/src/ShortUrl/Model/Validation/ShortUrlInputFilter.php b/module/Core/src/ShortUrl/Model/Validation/ShortUrlInputFilter.php index 88b629e8..cfaed5f2 100644 --- a/module/Core/src/ShortUrl/Model/Validation/ShortUrlInputFilter.php +++ b/module/Core/src/ShortUrl/Model/Validation/ShortUrlInputFilter.php @@ -8,9 +8,7 @@ use DateTimeInterface; use Laminas\Filter; use Laminas\InputFilter\InputFilter; use Laminas\Validator; -use Shlinkio\Shlink\Common\Validation\HostAndPortValidator; use Shlinkio\Shlink\Common\Validation\InputFactory; -use Shlinkio\Shlink\Core\Config\Options\UrlShortenerOptions; use Shlinkio\Shlink\Rest\Entity\ApiKey; use function is_string; @@ -18,16 +16,15 @@ use function preg_match; use function substr; use const Shlinkio\Shlink\LOOSE_URI_MATCHER; -use const Shlinkio\Shlink\MIN_SHORT_CODES_LENGTH; -/** @extends InputFilter */ +/** + * @extends InputFilter + * @deprecated + */ class ShortUrlInputFilter extends InputFilter { // Fields for creation only public const string SHORT_CODE_LENGTH = 'shortCodeLength'; - public const string CUSTOM_SLUG = 'customSlug'; - public const string PATH_PREFIX = 'pathPrefix'; - public const string FIND_IF_EXISTS = 'findIfExists'; public const string DOMAIN = 'domain'; // Fields for creation and edition @@ -41,15 +38,6 @@ class ShortUrlInputFilter extends InputFilter public const string FORWARD_QUERY = 'forwardQuery'; public const string API_KEY = 'apiKey'; - public static function forCreation(array $data, UrlShortenerOptions $options): self - { - $instance = new self(); - $instance->initializeForCreation($options); - $instance->setData($data); - - return $instance; - } - public static function forEdition(array $data): self { $instance = new self(); @@ -59,38 +47,6 @@ class ShortUrlInputFilter extends InputFilter return $instance; } - private function initializeForCreation(UrlShortenerOptions $options): void - { - // The only way to enforce the NotEmpty validator to be evaluated when the key is present with an empty value - // is with setContinueIfEmpty(true) - $customSlug = InputFactory::basic(self::CUSTOM_SLUG)->setContinueIfEmpty(true); - $customSlug->getFilterChain()->attach(new CustomSlugFilter($options)); - $customSlug->getValidatorChain() - ->attach(new Validator\NotEmpty([ - Validator\NotEmpty::STRING, - Validator\NotEmpty::SPACE, - ])) - ->attach(CustomSlugValidator::forUrlShortenerOptions($options)); - $this->add($customSlug); - - // The path prefix is subject to the same filtering and validation logic as the custom slug, which takes into - // consideration if multi-segment slugs are enabled or not. - // The only difference is that empty values are allowed here. - $pathPrefix = InputFactory::basic(self::PATH_PREFIX); - $pathPrefix->getFilterChain()->attach(new CustomSlugFilter($options)); - $pathPrefix->getValidatorChain()->attach(CustomSlugValidator::forUrlShortenerOptions($options)); - $this->add($pathPrefix); - - $this->add(InputFactory::numeric(self::SHORT_CODE_LENGTH, min: MIN_SHORT_CODES_LENGTH)); - $this->add(InputFactory::boolean(self::FIND_IF_EXISTS)); - - $domain = InputFactory::basic(self::DOMAIN); - $domain->getValidatorChain()->attach(new HostAndPortValidator()); - $this->add($domain); - - $this->initializeForEdition(requireLongUrl: true); - } - private function initializeForEdition(bool $requireLongUrl = false): void { $longUrlInput = InputFactory::basic(self::LONG_URL, required: $requireLongUrl); diff --git a/module/Core/test-db/Domain/Repository/DomainRepositoryTest.php b/module/Core/test-db/Domain/Repository/DomainRepositoryTest.php index ebb53c10..4d404505 100644 --- a/module/Core/test-db/Domain/Repository/DomainRepositoryTest.php +++ b/module/Core/test-db/Domain/Repository/DomainRepositoryTest.php @@ -131,8 +131,10 @@ class DomainRepositoryTest extends DatabaseTestCase private function createShortUrl(Domain $domain, ApiKey|null $apiKey = null): ShortUrl { return ShortUrl::create( - ShortUrlCreation::fromRawData( - ['domain' => $domain->authority, 'apiKey' => $apiKey, 'longUrl' => 'https://foo'], + new ShortUrlCreation( + longUrl: 'https://foo', + domain: $domain->authority, + apiKey: $apiKey, ), new class ($domain) implements ShortUrlRelationResolverInterface { public function __construct(private Domain $domain) diff --git a/module/Core/test-db/ShortUrl/Repository/CrawlableShortCodesQueryTest.php b/module/Core/test-db/ShortUrl/Repository/CrawlableShortCodesQueryTest.php index 60955dd1..2adfd7f4 100644 --- a/module/Core/test-db/ShortUrl/Repository/CrawlableShortCodesQueryTest.php +++ b/module/Core/test-db/ShortUrl/Repository/CrawlableShortCodesQueryTest.php @@ -23,7 +23,7 @@ class CrawlableShortCodesQueryTest extends DatabaseTestCase public function invokingQueryReturnsExpectedResult(): void { $createShortUrl = fn (bool $crawlable) => ShortUrl::create( - ShortUrlCreation::fromRawData(['crawlable' => $crawlable, 'longUrl' => 'https://foo.com']), + new ShortUrlCreation('https://foo.com', crawlable: $crawlable), ); $shortUrl1 = $createShortUrl(true); diff --git a/module/Core/test-db/ShortUrl/Repository/DeleteExpiredShortUrlsRepositoryTest.php b/module/Core/test-db/ShortUrl/Repository/DeleteExpiredShortUrlsRepositoryTest.php index 2e10d935..e123d70f 100644 --- a/module/Core/test-db/ShortUrl/Repository/DeleteExpiredShortUrlsRepositoryTest.php +++ b/module/Core/test-db/ShortUrl/Repository/DeleteExpiredShortUrlsRepositoryTest.php @@ -10,7 +10,6 @@ use PHPUnit\Framework\Attributes\TestWith; use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl; use Shlinkio\Shlink\Core\ShortUrl\Model\ExpiredShortUrlsConditions; use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlCreation; -use Shlinkio\Shlink\Core\ShortUrl\Model\Validation\ShortUrlInputFilter; use Shlinkio\Shlink\Core\ShortUrl\Repository\ExpiredShortUrlsRepository; use Shlinkio\Shlink\Core\Visit\Entity\Visit; use Shlinkio\Shlink\Core\Visit\Model\Visitor; @@ -62,19 +61,19 @@ class DeleteExpiredShortUrlsRepositoryTest extends DatabaseTestCase { // Create some non-expired short URLs $this->createShortUrls(5); - $this->createShortUrls(2, [ShortUrlInputFilter::VALID_UNTIL => Chronos::now()->addDays(1)->toAtomString()]); - $this->createShortUrls(3, [ShortUrlInputFilter::MAX_VISITS => 4], visitsPerShortUrl: 2); + $this->createShortUrls(2, ['validUntil' => Chronos::now()->addDays(1)]); + $this->createShortUrls(3, ['maxVisits' => 4], visitsPerShortUrl: 2); // Create some short URLs with a valid date in the past - $this->createShortUrls(3, [ShortUrlInputFilter::VALID_UNTIL => Chronos::now()->subDays(1)->toAtomString()]); + $this->createShortUrls(3, ['validUntil' => Chronos::now()->subDays(1)]); // Create some short URLs which reached the max amount of visits - $this->createShortUrls(2, [ShortUrlInputFilter::MAX_VISITS => 3], visitsPerShortUrl: 3); + $this->createShortUrls(2, ['maxVisits' => 3], visitsPerShortUrl: 3); // Create some short URLs with a valid date in the past which also reached the max amount of visits $this->createShortUrls(4, [ - ShortUrlInputFilter::VALID_UNTIL => Chronos::now()->subDays(1)->toAtomString(), - ShortUrlInputFilter::MAX_VISITS => 3, + 'validUntil' => Chronos::now()->subDays(1), + 'maxVisits' => 3, ], visitsPerShortUrl: 4); $this->getEntityManager()->flush(); @@ -85,10 +84,7 @@ class DeleteExpiredShortUrlsRepositoryTest extends DatabaseTestCase private function createShortUrls(int $amountOfShortUrls, array $metadata = [], int $visitsPerShortUrl = 0): void { for ($i = 0; $i < $amountOfShortUrls; $i++) { - $shortUrl = ShortUrl::create(ShortUrlCreation::fromRawData([ - ShortUrlInputFilter::LONG_URL => 'https://shlink.io', - ...$metadata, - ])); + $shortUrl = ShortUrl::create(new ShortUrlCreation('https://shlink.io', ...$metadata)); $this->getEntityManager()->persist($shortUrl); for ($j = 0; $j < $visitsPerShortUrl; $j++) { diff --git a/module/Core/test-db/ShortUrl/Repository/ShortUrlListRepositoryTest.php b/module/Core/test-db/ShortUrl/Repository/ShortUrlListRepositoryTest.php index 11cbfde1..62a2d084 100644 --- a/module/Core/test-db/ShortUrl/Repository/ShortUrlListRepositoryTest.php +++ b/module/Core/test-db/ShortUrl/Repository/ShortUrlListRepositoryTest.php @@ -60,7 +60,7 @@ class ShortUrlListRepositoryTest extends DatabaseTestCase public function findListProperlyFiltersResult(): void { $foo = ShortUrl::create( - ShortUrlCreation::fromRawData(['longUrl' => 'https://foo', 'tags' => ['bar']]), + new ShortUrlCreation('https://foo', tags: ['bar']), $this->relationResolver, ); $this->getEntityManager()->persist($foo); @@ -163,30 +163,27 @@ class ShortUrlListRepositoryTest extends DatabaseTestCase #[Test] public function findListReturnsOnlyThoseWithMatchingTags(): void { - $shortUrl1 = ShortUrl::create(ShortUrlCreation::fromRawData([ - 'longUrl' => 'https://foo1', - 'tags' => ['foo', 'bar'], - ]), $this->relationResolver); + $shortUrl1 = ShortUrl::create( + new ShortUrlCreation('https://foo1', tags: ['foo', 'bar']), + $this->relationResolver, + ); $this->getEntityManager()->persist($shortUrl1); - $shortUrl2 = ShortUrl::create(ShortUrlCreation::fromRawData([ - 'longUrl' => 'https://foo2', - 'tags' => ['foo', 'baz'], - ]), $this->relationResolver); + $shortUrl2 = ShortUrl::create( + new ShortUrlCreation('https://foo2', tags: ['foo', 'baz']), + $this->relationResolver, + ); $this->getEntityManager()->persist($shortUrl2); - $shortUrl3 = ShortUrl::create(ShortUrlCreation::fromRawData([ - 'longUrl' => 'https://foo3', - 'tags' => ['foo'], - ]), $this->relationResolver); + $shortUrl3 = ShortUrl::create(new ShortUrlCreation('https://foo3', tags: ['foo']), $this->relationResolver); $this->getEntityManager()->persist($shortUrl3); - $shortUrl4 = ShortUrl::create(ShortUrlCreation::fromRawData([ - 'longUrl' => 'https://foo4', - 'tags' => ['bar', 'baz'], - ]), $this->relationResolver); + $shortUrl4 = ShortUrl::create( + new ShortUrlCreation('https://foo4', tags: ['bar', 'baz']), + $this->relationResolver, + ); $this->getEntityManager()->persist($shortUrl4); - $shortUrl5 = ShortUrl::create(ShortUrlCreation::fromRawData([ - 'longUrl' => 'https://foo5', - 'tags' => ['bar', 'baz'], - ]), $this->relationResolver); + $shortUrl5 = ShortUrl::create( + new ShortUrlCreation('https://foo5', tags: ['bar', 'baz']), + $this->relationResolver, + ); $this->getEntityManager()->persist($shortUrl5); $this->getEntityManager()->flush(); @@ -264,20 +261,14 @@ class ShortUrlListRepositoryTest extends DatabaseTestCase #[Test] public function findListReturnsOnlyThoseWithMatchingDomains(): void { - $shortUrl1 = ShortUrl::create(ShortUrlCreation::fromRawData([ - 'longUrl' => 'https://foo1', - 'domain' => null, - ]), $this->relationResolver); + $shortUrl1 = ShortUrl::create(new ShortUrlCreation('https://foo1', domain: null), $this->relationResolver); $this->getEntityManager()->persist($shortUrl1); - $shortUrl2 = ShortUrl::create(ShortUrlCreation::fromRawData([ - 'longUrl' => 'https://foo2', - 'domain' => null, - ]), $this->relationResolver); + $shortUrl2 = ShortUrl::create(new ShortUrlCreation('https://foo2', domain: null), $this->relationResolver); $this->getEntityManager()->persist($shortUrl2); - $shortUrl3 = ShortUrl::create(ShortUrlCreation::fromRawData([ - 'longUrl' => 'https://foo3', - 'domain' => 'another.com', - ]), $this->relationResolver); + $shortUrl3 = ShortUrl::create( + new ShortUrlCreation('https://foo3', domain: 'another.com'), + $this->relationResolver, + ); $this->getEntityManager()->persist($shortUrl3); $this->getEntityManager()->flush(); @@ -304,25 +295,23 @@ class ShortUrlListRepositoryTest extends DatabaseTestCase #[Test] public function findListReturnsOnlyThoseWithoutExcludedUrls(): void { - $shortUrl1 = ShortUrl::create(ShortUrlCreation::fromRawData([ - 'longUrl' => 'https://foo1', - 'validUntil' => Chronos::now()->addDays(1)->toAtomString(), - 'maxVisits' => 100, - ]), $this->relationResolver); + $shortUrl1 = ShortUrl::create(new ShortUrlCreation( + longUrl: 'https://foo1', + validUntil: Chronos::now()->addDays(1), + maxVisits: 100, + ), $this->relationResolver); $this->getEntityManager()->persist($shortUrl1); - $shortUrl2 = ShortUrl::create(ShortUrlCreation::fromRawData([ - 'longUrl' => 'https://foo2', - 'validUntil' => Chronos::now()->subDays(1)->toAtomString(), - ]), $this->relationResolver); + $shortUrl2 = ShortUrl::create(new ShortUrlCreation( + longUrl: 'https://foo2', + validUntil: Chronos::now()->subDays(1), + ), $this->relationResolver); $this->getEntityManager()->persist($shortUrl2); - $shortUrl3 = ShortUrl::create(ShortUrlCreation::fromRawData([ - 'longUrl' => 'https://foo3', - ]), $this->relationResolver); + $shortUrl3 = ShortUrl::create(new ShortUrlCreation('https://foo3'), $this->relationResolver); $this->getEntityManager()->persist($shortUrl3); - $shortUrl4 = ShortUrl::create(ShortUrlCreation::fromRawData([ - 'longUrl' => 'https://foo4', - 'maxVisits' => 3, - ]), $this->relationResolver); + $shortUrl4 = ShortUrl::create(new ShortUrlCreation( + longUrl: 'https://foo4', + maxVisits: 3, + ), $this->relationResolver); $this->getEntityManager()->persist($shortUrl4); $this->getEntityManager()->persist(Visit::forValidShortUrl($shortUrl4, Visitor::empty())); $this->getEntityManager()->persist(Visit::forValidShortUrl($shortUrl4, Visitor::empty())); @@ -380,25 +369,13 @@ class ShortUrlListRepositoryTest extends DatabaseTestCase $apiKey3 = ApiKey::create(); $this->getEntityManager()->persist($apiKey3); - $shortUrl1 = ShortUrl::create(ShortUrlCreation::fromRawData([ - 'longUrl' => 'https://foo1', - 'apiKey' => $apiKey1, - ]), $this->relationResolver); + $shortUrl1 = ShortUrl::create(new ShortUrlCreation('https://foo1', apiKey: $apiKey1), $this->relationResolver); $this->getEntityManager()->persist($shortUrl1); - $shortUrl2 = ShortUrl::create(ShortUrlCreation::fromRawData([ - 'longUrl' => 'https://foo2', - 'apiKey' => $apiKey1, - ]), $this->relationResolver); + $shortUrl2 = ShortUrl::create(new ShortUrlCreation('https://foo2', apiKey: $apiKey1), $this->relationResolver); $this->getEntityManager()->persist($shortUrl2); - $shortUrl3 = ShortUrl::create(ShortUrlCreation::fromRawData([ - 'longUrl' => 'https://foo3', - 'apiKey' => $apiKey2, - ]), $this->relationResolver); + $shortUrl3 = ShortUrl::create(new ShortUrlCreation('https://foo3', apiKey: $apiKey2), $this->relationResolver); $this->getEntityManager()->persist($shortUrl3); - $shortUrl4 = ShortUrl::create(ShortUrlCreation::fromRawData([ - 'longUrl' => 'https://foo4', - 'apiKey' => $apiKey1, - ]), $this->relationResolver); + $shortUrl4 = ShortUrl::create(new ShortUrlCreation('https://foo4', apiKey: $apiKey1), $this->relationResolver); $this->getEntityManager()->persist($shortUrl4); $this->getEntityManager()->flush(); diff --git a/module/Core/test-db/ShortUrl/Repository/ShortUrlRepositoryTest.php b/module/Core/test-db/ShortUrl/Repository/ShortUrlRepositoryTest.php index 535ca50f..f367de71 100644 --- a/module/Core/test-db/ShortUrl/Repository/ShortUrlRepositoryTest.php +++ b/module/Core/test-db/ShortUrl/Repository/ShortUrlRepositoryTest.php @@ -34,18 +34,20 @@ class ShortUrlRepositoryTest extends DatabaseTestCase #[Test] public function findOneWithDomainFallbackReturnsProperData(): void { - $regularOne = ShortUrl::create( - ShortUrlCreation::fromRawData(['customSlug' => 'Foo', 'longUrl' => 'https://foo']), - ); + $regularOne = ShortUrl::create(new ShortUrlCreation(longUrl: 'https://foo', customSlug: 'Foo')); $this->getEntityManager()->persist($regularOne); - $withDomain = ShortUrl::create(ShortUrlCreation::fromRawData( - ['domain' => 'example.com', 'customSlug' => 'domain-short-code', 'longUrl' => 'https://foo'], + $withDomain = ShortUrl::create(new ShortUrlCreation( + longUrl: 'https://foo', + customSlug: 'domain-short-code', + domain: 'example.com', )); $this->getEntityManager()->persist($withDomain); - $withDomainDuplicatingRegular = ShortUrl::create(ShortUrlCreation::fromRawData( - ['domain' => 's.test', 'customSlug' => 'Foo', 'longUrl' => 'https://foo_with_domain'], + $withDomainDuplicatingRegular = ShortUrl::create(new ShortUrlCreation( + longUrl: 'https://foo_with_domain', + customSlug: 'Foo', + domain: 's.test', )); $this->getEntityManager()->persist($withDomainDuplicatingRegular); @@ -103,13 +105,13 @@ class ShortUrlRepositoryTest extends DatabaseTestCase #[Test] public function shortCodeIsInUseLooksForShortUrlInProperSetOfTables(): void { - $shortUrlWithoutDomain = ShortUrl::create( - ShortUrlCreation::fromRawData(['customSlug' => 'my-cool-slug', 'longUrl' => 'https://foo']), - ); + $shortUrlWithoutDomain = ShortUrl::create(new ShortUrlCreation('https://foo', customSlug: 'my-cool-slug')); $this->getEntityManager()->persist($shortUrlWithoutDomain); - $shortUrlWithDomain = ShortUrl::create(ShortUrlCreation::fromRawData( - ['domain' => 's.test', 'customSlug' => 'another-slug', 'longUrl' => 'https://foo'], + $shortUrlWithDomain = ShortUrl::create(new ShortUrlCreation( + longUrl: 'https://foo', + customSlug: 'another-slug', + domain: 's.test', )); $this->getEntityManager()->persist($shortUrlWithDomain); @@ -133,13 +135,13 @@ class ShortUrlRepositoryTest extends DatabaseTestCase public function findOneLooksForShortUrlInProperSetOfTables(): void { $shortUrlWithoutDomain = ShortUrl::create( - ShortUrlCreation::fromRawData(['customSlug' => 'my-cool-slug', 'longUrl' => 'https://foo']), + new ShortUrlCreation(longUrl: 'https://foo', customSlug: 'my-cool-slug'), ); $this->getEntityManager()->persist($shortUrlWithoutDomain); - $shortUrlWithDomain = ShortUrl::create(ShortUrlCreation::fromRawData( - ['domain' => 's.test', 'customSlug' => 'another-slug', 'longUrl' => 'https://foo'], - )); + $shortUrlWithDomain = ShortUrl::create( + new ShortUrlCreation(longUrl: 'https://foo', customSlug: 'another-slug', domain: 's.test'), + ); $this->getEntityManager()->persist($shortUrlWithDomain); $this->getEntityManager()->flush(); @@ -159,16 +161,14 @@ class ShortUrlRepositoryTest extends DatabaseTestCase #[Test] public function findOneMatchingReturnsNullForNonExistingShortUrls(): void { - self::assertNull($this->repo->findOneMatching(ShortUrlCreation::fromRawData(['longUrl' => 'https://foobar']))); - self::assertNull($this->repo->findOneMatching( - ShortUrlCreation::fromRawData(['longUrl' => 'https://foobar', 'tags' => ['foo', 'bar']]), - )); - self::assertNull($this->repo->findOneMatching(ShortUrlCreation::fromRawData([ - 'validSince' => Chronos::parse('2020-03-05 20:18:30'), - 'customSlug' => 'this_slug_does_not_exist', - 'longUrl' => 'https://foobar', - 'tags' => ['foo', 'bar'], - ]))); + self::assertNull($this->repo->findOneMatching(new ShortUrlCreation('https://foobar'))); + self::assertNull($this->repo->findOneMatching(new ShortUrlCreation('https://foobar', tags: ['foo', 'bar']))); + self::assertNull($this->repo->findOneMatching(new ShortUrlCreation( + longUrl: 'https://foobar', + validSince: Chronos::parse('2020-03-05 20:18:30'), + customSlug: 'this_slug_does_not_exist', + tags: ['foo', 'bar'], + ))); } #[Test] @@ -177,31 +177,30 @@ class ShortUrlRepositoryTest extends DatabaseTestCase $start = Chronos::parse('2020-03-05 20:18:30'); $end = Chronos::parse('2021-03-05 20:18:30'); - $shortUrl = ShortUrl::create(ShortUrlCreation::fromRawData( - ['validSince' => $start, 'longUrl' => 'https://foo', 'tags' => ['foo', 'bar']], - ), $this->relationResolver); + $shortUrl = ShortUrl::create( + new ShortUrlCreation(longUrl: 'https://foo', validSince: $start, tags: ['foo', 'bar']), + $this->relationResolver, + ); $this->getEntityManager()->persist($shortUrl); - $shortUrl2 = ShortUrl::create( - ShortUrlCreation::fromRawData(['validUntil' => $end, 'longUrl' => 'https://bar']), - ); + $shortUrl2 = ShortUrl::create(new ShortUrlCreation(longUrl: 'https://bar', validUntil: $end)); $this->getEntityManager()->persist($shortUrl2); $shortUrl3 = ShortUrl::create( - ShortUrlCreation::fromRawData(['validSince' => $start, 'validUntil' => $end, 'longUrl' => 'https://baz']), + new ShortUrlCreation(longUrl: 'https://baz', validSince: $start, validUntil: $end), ); $this->getEntityManager()->persist($shortUrl3); $shortUrl4 = ShortUrl::create( - ShortUrlCreation::fromRawData(['customSlug' => 'custom', 'validUntil' => $end, 'longUrl' => 'https://foo']), + new ShortUrlCreation(longUrl: 'https://foo', validUntil: $end, customSlug: 'custom'), ); $this->getEntityManager()->persist($shortUrl4); - $shortUrl5 = ShortUrl::create(ShortUrlCreation::fromRawData(['maxVisits' => 3, 'longUrl' => 'https://foo'])); + $shortUrl5 = ShortUrl::create(new ShortUrlCreation(longUrl: 'https://foo', maxVisits: 3)); $this->getEntityManager()->persist($shortUrl5); $shortUrl6 = ShortUrl::create( - ShortUrlCreation::fromRawData(['domain' => 's.test', 'longUrl' => 'https://foo']), + new ShortUrlCreation(longUrl: 'https://foo', domain: 's.test'), ); $this->getEntityManager()->persist($shortUrl6); @@ -209,41 +208,39 @@ class ShortUrlRepositoryTest extends DatabaseTestCase self::assertSame( $shortUrl, - $this->repo->findOneMatching(ShortUrlCreation::fromRawData( - ['validSince' => $start, 'longUrl' => 'https://foo', 'tags' => ['foo', 'bar']], - )), + $this->repo->findOneMatching( + new ShortUrlCreation(longUrl: 'https://foo', validSince: $start, tags: ['foo', 'bar']), + ), ); self::assertSame( $shortUrl2, $this->repo->findOneMatching( - ShortUrlCreation::fromRawData(['validUntil' => $end, 'longUrl' => 'https://bar']), + new ShortUrlCreation(longUrl: 'https://bar', validUntil: $end), ), ); self::assertSame( $shortUrl3, - $this->repo->findOneMatching(ShortUrlCreation::fromRawData([ - 'validSince' => $start, - 'validUntil' => $end, - 'longUrl' => 'https://baz', - ])), + $this->repo->findOneMatching(new ShortUrlCreation( + longUrl: 'https://baz', + validSince: $start, + validUntil: $end, + )), ); self::assertSame( $shortUrl4, - $this->repo->findOneMatching(ShortUrlCreation::fromRawData([ - 'customSlug' => 'custom', - 'validUntil' => $end, - 'longUrl' => 'https://foo', - ])), + $this->repo->findOneMatching(new ShortUrlCreation( + longUrl: 'https://foo', + validUntil: $end, + customSlug: 'custom', + )), ); self::assertSame( $shortUrl5, - $this->repo->findOneMatching(ShortUrlCreation::fromRawData(['maxVisits' => 3, 'longUrl' => 'https://foo'])), + $this->repo->findOneMatching(new ShortUrlCreation(longUrl: 'https://foo', maxVisits: 3)), ); self::assertSame( $shortUrl6, - $this->repo->findOneMatching( - ShortUrlCreation::fromRawData(['domain' => 's.test', 'longUrl' => 'https://foo']), - ), + $this->repo->findOneMatching(new ShortUrlCreation(longUrl: 'https://foo', domain: 's.test')), ); } @@ -252,9 +249,7 @@ class ShortUrlRepositoryTest extends DatabaseTestCase { $start = Chronos::parse('2020-03-05 20:18:30'); $tags = ['foo', 'bar']; - $meta = ShortUrlCreation::fromRawData( - ['validSince' => $start, 'maxVisits' => 50, 'longUrl' => 'https://foo', 'tags' => $tags], - ); + $meta = new ShortUrlCreation('https://foo', validSince: $start, maxVisits: 50, tags: $tags); $shortUrl1 = ShortUrl::create($meta, $this->relationResolver); $this->getEntityManager()->persist($shortUrl1); @@ -298,106 +293,97 @@ class ShortUrlRepositoryTest extends DatabaseTestCase $adminApiKey = ApiKey::create(); $this->getEntityManager()->persist($adminApiKey); - $shortUrl = ShortUrl::create(ShortUrlCreation::fromRawData([ - 'validSince' => $start, - 'apiKey' => $apiKey, - 'domain' => $rightDomain->authority, - 'longUrl' => 'https://foo', - 'tags' => ['foo', 'bar'], - ]), $this->relationResolver); + $shortUrl = ShortUrl::create(new ShortUrlCreation( + longUrl: 'https://foo', + validSince: $start, + domain: $rightDomain->authority, + apiKey: $apiKey, + tags: ['foo', 'bar'], + ), $this->relationResolver); $this->getEntityManager()->persist($shortUrl); - $nonDomainShortUrl = ShortUrl::create(ShortUrlCreation::fromRawData([ - 'apiKey' => $apiKey, - 'longUrl' => 'https://non-domain', - ]), $this->relationResolver); + $nonDomainShortUrl = ShortUrl::create(new ShortUrlCreation( + longUrl: 'https://non-domain', + apiKey: $apiKey, + ), $this->relationResolver); $this->getEntityManager()->persist($nonDomainShortUrl); $this->getEntityManager()->flush(); self::assertSame( $shortUrl, - $this->repo->findOneMatching(ShortUrlCreation::fromRawData( - ['validSince' => $start, 'longUrl' => 'https://foo', 'tags' => ['foo', 'bar']], + $this->repo->findOneMatching( + new ShortUrlCreation(longUrl: 'https://foo', validSince: $start, tags: ['foo', 'bar']), + ), + ); + self::assertSame($shortUrl, $this->repo->findOneMatching(new ShortUrlCreation( + longUrl: 'https://foo', + validSince: $start, + apiKey: $apiKey, + tags: ['foo', 'bar'], + ))); + self::assertSame($shortUrl, $this->repo->findOneMatching(new ShortUrlCreation( + longUrl: 'https://foo', + validSince: $start, + apiKey: $adminApiKey, + tags: ['foo', 'bar'], + ))); + self::assertNull($this->repo->findOneMatching(new ShortUrlCreation( + longUrl: 'https://foo', + validSince: $start, + apiKey: $otherApiKey, + tags: ['foo', 'bar'], + ))); + + self::assertSame( + $shortUrl, + $this->repo->findOneMatching(new ShortUrlCreation( + longUrl: 'https://foo', + validSince: $start, + domain: $rightDomain->authority, + tags: ['foo', 'bar'], )), ); - self::assertSame($shortUrl, $this->repo->findOneMatching(ShortUrlCreation::fromRawData([ - 'validSince' => $start, - 'apiKey' => $apiKey, - 'longUrl' => 'https://foo', - 'tags' => ['foo', 'bar'], - ]))); - self::assertSame($shortUrl, $this->repo->findOneMatching(ShortUrlCreation::fromRawData([ - 'validSince' => $start, - 'apiKey' => $adminApiKey, - 'longUrl' => 'https://foo', - 'tags' => ['foo', 'bar'], - ]))); - self::assertNull($this->repo->findOneMatching(ShortUrlCreation::fromRawData([ - 'validSince' => $start, - 'apiKey' => $otherApiKey, - 'longUrl' => 'https://foo', - 'tags' => ['foo', 'bar'], - ]))); - self::assertSame( $shortUrl, - $this->repo->findOneMatching(ShortUrlCreation::fromRawData([ - 'validSince' => $start, - 'domain' => $rightDomain->authority, - 'longUrl' => 'https://foo', - 'tags' => ['foo', 'bar'], - ])), + $this->repo->findOneMatching(new ShortUrlCreation( + longUrl: 'https://foo', + validSince: $start, + domain: $rightDomain->authority, + apiKey: $rightDomainApiKey, + tags: ['foo', 'bar'], + )), ); self::assertSame( $shortUrl, - $this->repo->findOneMatching(ShortUrlCreation::fromRawData([ - 'validSince' => $start, - 'domain' => $rightDomain->authority, - 'apiKey' => $rightDomainApiKey, - 'longUrl' => 'https://foo', - 'tags' => ['foo', 'bar'], - ])), - ); - self::assertSame( - $shortUrl, - $this->repo->findOneMatching(ShortUrlCreation::fromRawData([ - 'validSince' => $start, - 'domain' => $rightDomain->authority, - 'apiKey' => $apiKey, - 'longUrl' => 'https://foo', - 'tags' => ['foo', 'bar'], - ])), + $this->repo->findOneMatching(new ShortUrlCreation( + longUrl: 'https://foo', + validSince: $start, + domain: $rightDomain->authority, + apiKey: $apiKey, + tags: ['foo', 'bar'], + )), ); self::assertNull( - $this->repo->findOneMatching(ShortUrlCreation::fromRawData([ - 'validSince' => $start, - 'domain' => $rightDomain->authority, - 'apiKey' => $wrongDomainApiKey, - 'longUrl' => 'https://foo', - 'tags' => ['foo', 'bar'], - ])), + $this->repo->findOneMatching(new ShortUrlCreation( + longUrl: 'https://foo', + validSince: $start, + domain: $rightDomain->authority, + apiKey: $wrongDomainApiKey, + tags: ['foo', 'bar'], + )), ); self::assertSame( $nonDomainShortUrl, - $this->repo->findOneMatching(ShortUrlCreation::fromRawData([ - 'apiKey' => $apiKey, - 'longUrl' => 'https://non-domain', - ])), + $this->repo->findOneMatching(new ShortUrlCreation('https://non-domain', apiKey: $apiKey)), ); self::assertSame( $nonDomainShortUrl, - $this->repo->findOneMatching(ShortUrlCreation::fromRawData([ - 'apiKey' => $adminApiKey, - 'longUrl' => 'https://non-domain', - ])), + $this->repo->findOneMatching(new ShortUrlCreation('https://non-domain', apiKey: $adminApiKey)), ); self::assertNull( - $this->repo->findOneMatching(ShortUrlCreation::fromRawData([ - 'apiKey' => $otherApiKey, - 'longUrl' => 'https://non-domain', - ])), + $this->repo->findOneMatching(new ShortUrlCreation('https://non-domain', apiKey: $otherApiKey)), ); } diff --git a/module/Core/test-db/Tag/Repository/TagRepositoryTest.php b/module/Core/test-db/Tag/Repository/TagRepositoryTest.php index 224e0c11..69f1b381 100644 --- a/module/Core/test-db/Tag/Repository/TagRepositoryTest.php +++ b/module/Core/test-db/Tag/Repository/TagRepositoryTest.php @@ -73,9 +73,8 @@ class TagRepositoryTest extends DatabaseTestCase [$firstUrlTags] = array_chunk($names, 3); $secondUrlTags = [$names[0]]; - $metaWithTags = static fn (array $tags, ApiKey|null $apiKey) => ShortUrlCreation::fromRawData( - ['longUrl' => 'https://longUrl', 'tags' => $tags, 'apiKey' => $apiKey], - ); + $metaWithTags = static fn (array $tags, ApiKey|null $apiKey) => + new ShortUrlCreation('https://longUrl', apiKey: $apiKey, tags: $tags); $shortUrl = ShortUrl::create($metaWithTags($firstUrlTags, $apiKey), $this->relationResolver); $this->getEntityManager()->persist($shortUrl); @@ -227,15 +226,14 @@ class TagRepositoryTest extends DatabaseTestCase [$firstUrlTags, $secondUrlTags] = array_chunk($names, 3); - $shortUrl = ShortUrl::create(ShortUrlCreation::fromRawData( - ['apiKey' => $authorApiKey, 'longUrl' => 'https://longUrl', 'tags' => $firstUrlTags], - ), $this->relationResolver); + $shortUrl = ShortUrl::create( + new ShortUrlCreation(longUrl: 'https://longUrl', apiKey: $authorApiKey, tags: $firstUrlTags), + $this->relationResolver, + ); $this->getEntityManager()->persist($shortUrl); $shortUrl2 = ShortUrl::create( - ShortUrlCreation::fromRawData( - ['domain' => $domain->authority, 'longUrl' => 'https://longUrl', 'tags' => $secondUrlTags], - ), + new ShortUrlCreation('https://longUrl', domain: $domain->authority, tags: $secondUrlTags), $this->relationResolver, ); $this->getEntityManager()->persist($shortUrl2); diff --git a/module/Core/test-db/Visit/Repository/VisitDeleterRepositoryTest.php b/module/Core/test-db/Visit/Repository/VisitDeleterRepositoryTest.php index 58d52713..8ba5ae16 100644 --- a/module/Core/test-db/Visit/Repository/VisitDeleterRepositoryTest.php +++ b/module/Core/test-db/Visit/Repository/VisitDeleterRepositoryTest.php @@ -7,7 +7,6 @@ namespace ShlinkioDbTest\Shlink\Core\Visit\Repository; use PHPUnit\Framework\Attributes\Test; use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl; use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlCreation; -use Shlinkio\Shlink\Core\ShortUrl\Model\Validation\ShortUrlInputFilter; use Shlinkio\Shlink\Core\ShortUrl\Resolver\PersistenceShortUrlRelationResolver; use Shlinkio\Shlink\Core\Visit\Entity\OrphanVisitsCount; use Shlinkio\Shlink\Core\Visit\Entity\ShortUrlVisitsCount; @@ -39,21 +38,21 @@ class VisitDeleterRepositoryTest extends DatabaseTestCase $this->getEntityManager()->persist(Visit::forValidShortUrl($shortUrl1, Visitor::empty())); $this->getEntityManager()->persist(Visit::forValidShortUrl($shortUrl1, Visitor::empty())); - $shortUrl2 = ShortUrl::create(ShortUrlCreation::fromRawData([ - ShortUrlInputFilter::LONG_URL => 'https://foo.com', - ShortUrlInputFilter::DOMAIN => 's.test', - ShortUrlInputFilter::CUSTOM_SLUG => 'foo', - ]), new PersistenceShortUrlRelationResolver($this->getEntityManager())); + $shortUrl2 = ShortUrl::create(new ShortUrlCreation( + 'https://foo.com', + customSlug: 'foo', + domain: 's.test', + ), new PersistenceShortUrlRelationResolver($this->getEntityManager())); $this->getEntityManager()->persist($shortUrl2); $this->getEntityManager()->persist(Visit::forValidShortUrl($shortUrl2, Visitor::empty())); $this->getEntityManager()->persist(Visit::forValidShortUrl($shortUrl2, Visitor::empty())); $this->getEntityManager()->persist(Visit::forValidShortUrl($shortUrl2, Visitor::empty())); $this->getEntityManager()->persist(Visit::forValidShortUrl($shortUrl2, Visitor::empty())); - $shortUrl3 = ShortUrl::create(ShortUrlCreation::fromRawData([ - ShortUrlInputFilter::LONG_URL => 'https://foo.com', - ShortUrlInputFilter::CUSTOM_SLUG => 'foo', - ]), new PersistenceShortUrlRelationResolver($this->getEntityManager())); + $shortUrl3 = ShortUrl::create(new ShortUrlCreation( + 'https://foo.com', + customSlug: 'foo', + ), new PersistenceShortUrlRelationResolver($this->getEntityManager())); $this->getEntityManager()->persist($shortUrl3); $this->getEntityManager()->persist(Visit::forValidShortUrl($shortUrl3, Visitor::empty())); diff --git a/module/Core/test-db/Visit/Repository/VisitRepositoryTest.php b/module/Core/test-db/Visit/Repository/VisitRepositoryTest.php index 56b6175f..8f2e2598 100644 --- a/module/Core/test-db/Visit/Repository/VisitRepositoryTest.php +++ b/module/Core/test-db/Visit/Repository/VisitRepositoryTest.php @@ -11,7 +11,6 @@ use Shlinkio\Shlink\Core\Domain\Entity\Domain; use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl; use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlCreation; use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlIdentifier; -use Shlinkio\Shlink\Core\ShortUrl\Model\Validation\ShortUrlInputFilter; use Shlinkio\Shlink\Core\ShortUrl\Resolver\PersistenceShortUrlRelationResolver; use Shlinkio\Shlink\Core\Visit\Entity\OrphanVisitsCount; use Shlinkio\Shlink\Core\Visit\Entity\ShortUrlVisitsCount; @@ -205,18 +204,18 @@ class VisitRepositoryTest extends DatabaseTestCase { $foo = 'foo'; - $shortUrl1 = ShortUrl::create(ShortUrlCreation::fromRawData([ - ShortUrlInputFilter::LONG_URL => 'https://longUrl', - ShortUrlInputFilter::TAGS => [$foo], - ShortUrlInputFilter::DOMAIN => 'foo.com', - ]), $this->relationResolver); + $shortUrl1 = ShortUrl::create(new ShortUrlCreation( + 'https://longUrl', + domain: 'foo.com', + tags: [$foo], + ), $this->relationResolver); $this->getEntityManager()->persist($shortUrl1); $this->createVisitsForShortUrl($shortUrl1, 6); - $shortUrl2 = ShortUrl::create(ShortUrlCreation::fromRawData([ - ShortUrlInputFilter::LONG_URL => 'https://longUrl', - ShortUrlInputFilter::TAGS => [$foo], - ]), $this->relationResolver); + $shortUrl2 = ShortUrl::create(new ShortUrlCreation( + 'https://longUrl', + tags: [$foo], + ), $this->relationResolver); $this->getEntityManager()->persist($shortUrl2); $this->createVisitsForShortUrl($shortUrl2, 6); @@ -303,9 +302,7 @@ class VisitRepositoryTest extends DatabaseTestCase $apiKey1 = ApiKey::fromMeta(ApiKeyMeta::withRoles(RoleDefinition::forAuthoredShortUrls())); $this->getEntityManager()->persist($apiKey1); $shortUrl = ShortUrl::create( - ShortUrlCreation::fromRawData( - ['apiKey' => $apiKey1, 'domain' => $domain->authority, 'longUrl' => 'https://longUrl'], - ), + new ShortUrlCreation(longUrl: 'https://longUrl', domain: $domain->authority, apiKey: $apiKey1), $this->relationResolver, ); $this->getEntityManager()->persist($shortUrl); @@ -313,16 +310,12 @@ class VisitRepositoryTest extends DatabaseTestCase $apiKey2 = ApiKey::fromMeta(ApiKeyMeta::withRoles(RoleDefinition::forAuthoredShortUrls())); $this->getEntityManager()->persist($apiKey2); - $shortUrl2 = ShortUrl::create( - ShortUrlCreation::fromRawData(['apiKey' => $apiKey2, 'longUrl' => 'https://longUrl']), - ); + $shortUrl2 = ShortUrl::create(new ShortUrlCreation(longUrl: 'https://longUrl', apiKey: $apiKey2)); $this->getEntityManager()->persist($shortUrl2); $this->createVisitsForShortUrl($shortUrl2, 5); $shortUrl3 = ShortUrl::create( - ShortUrlCreation::fromRawData( - ['apiKey' => $apiKey2, 'domain' => $domain->authority, 'longUrl' => 'https://longUrl'], - ), + new ShortUrlCreation('https://longUrl', domain: $domain->authority, apiKey: $apiKey2), $this->relationResolver, ); $this->getEntityManager()->persist($shortUrl3); @@ -391,7 +384,7 @@ class VisitRepositoryTest extends DatabaseTestCase #[Test] public function findOrphanVisitsReturnsExpectedResult(): void { - $shortUrl = ShortUrl::create(ShortUrlCreation::fromRawData(['longUrl' => 'https://longUrl'])); + $shortUrl = ShortUrl::withLongUrl('https://longUrl'); $this->getEntityManager()->persist($shortUrl); $this->createVisitsForShortUrl($shortUrl, 7); @@ -458,7 +451,7 @@ class VisitRepositoryTest extends DatabaseTestCase #[Test] public function countOrphanVisitsReturnsExpectedResult(): void { - $shortUrl = ShortUrl::create(ShortUrlCreation::fromRawData(['longUrl' => 'https://longUrl'])); + $shortUrl = ShortUrl::withLongUrl('https://longUrl'); $this->getEntityManager()->persist($shortUrl); $this->createVisitsForShortUrl($shortUrl, 7); @@ -582,11 +575,11 @@ class VisitRepositoryTest extends DatabaseTestCase ApiKey|null $apiKey = null, int $visitsAmount = 6, ): array { - $shortUrl = ShortUrl::create(ShortUrlCreation::fromRawData([ - ShortUrlInputFilter::LONG_URL => 'https://longUrl', - ShortUrlInputFilter::TAGS => $tags, - ShortUrlInputFilter::API_KEY => $apiKey, - ]), $this->relationResolver); + $shortUrl = ShortUrl::create(new ShortUrlCreation( + 'https://longUrl', + apiKey: $apiKey, + tags: $tags, + ), $this->relationResolver); $domain = is_string($withDomain) ? $withDomain : 'example.com'; $shortCode = $shortUrl->getShortCode(); $this->getEntityManager()->persist($shortUrl); @@ -594,11 +587,11 @@ class VisitRepositoryTest extends DatabaseTestCase $this->createVisitsForShortUrl($shortUrl, $visitsAmount); if ($withDomain !== false) { - $shortUrlWithDomain = ShortUrl::create(ShortUrlCreation::fromRawData([ - 'customSlug' => $shortCode, - 'domain' => $domain, - 'longUrl' => 'https://longUrl', - ])); + $shortUrlWithDomain = ShortUrl::create(new ShortUrlCreation( + longUrl: 'https://longUrl', + customSlug: $shortCode, + domain: $domain, + )); $this->getEntityManager()->persist($shortUrlWithDomain); $this->createVisitsForShortUrl($shortUrlWithDomain, 3); $this->getEntityManager()->flush(); diff --git a/module/Core/test/EventDispatcher/PublishingUpdatesGeneratorTest.php b/module/Core/test/EventDispatcher/PublishingUpdatesGeneratorTest.php index 310c8b3f..dd6d0028 100644 --- a/module/Core/test/EventDispatcher/PublishingUpdatesGeneratorTest.php +++ b/module/Core/test/EventDispatcher/PublishingUpdatesGeneratorTest.php @@ -43,11 +43,11 @@ class PublishingUpdatesGeneratorTest extends TestCase #[Test, DataProvider('provideMethod')] public function visitIsProperlySerializedIntoUpdate(string $method, string $expectedTopic, string|null $title): void { - $shortUrl = ShortUrl::create(ShortUrlCreation::fromRawData([ - 'customSlug' => 'foo', - 'longUrl' => 'https://longUrl', - 'title' => $title, - ])); + $shortUrl = ShortUrl::create(new ShortUrlCreation( + longUrl: 'https://longUrl', + customSlug: 'foo', + title: $title, + )); $visit = Visit::forValidShortUrl($shortUrl, Visitor::empty()); /** @var Update $update */ @@ -123,11 +123,11 @@ class PublishingUpdatesGeneratorTest extends TestCase #[Test] public function shortUrlIsProperlySerializedIntoUpdate(): void { - $shortUrl = ShortUrl::create(ShortUrlCreation::fromRawData([ - 'customSlug' => 'foo', - 'longUrl' => 'https://longUrl', - 'title' => 'The title', - ])); + $shortUrl = ShortUrl::create(new ShortUrlCreation( + longUrl: 'https://longUrl', + customSlug: 'foo', + title: 'The title', + )); $update = $this->generator->newShortUrlUpdate($shortUrl); diff --git a/module/Core/test/EventDispatcher/RabbitMq/NotifyVisitToRabbitMqTest.php b/module/Core/test/EventDispatcher/RabbitMq/NotifyVisitToRabbitMqTest.php index 6dc1b7cb..e453df9c 100644 --- a/module/Core/test/EventDispatcher/RabbitMq/NotifyVisitToRabbitMqTest.php +++ b/module/Core/test/EventDispatcher/RabbitMq/NotifyVisitToRabbitMqTest.php @@ -101,10 +101,7 @@ class NotifyVisitToRabbitMqTest extends TestCase yield 'orphan visit' => [Visit::forBasePath($visitor), ['newOrphanVisitUpdate']]; yield 'non-orphan visit' => [ Visit::forValidShortUrl( - ShortUrl::create(ShortUrlCreation::fromRawData([ - 'longUrl' => 'https://foo', - 'customSlug' => 'bar', - ])), + ShortUrl::create(new ShortUrlCreation('https://foo', customSlug: 'bar')), $visitor, ), ['newShortUrlVisitUpdate', 'newVisitUpdate'], diff --git a/module/Core/test/Matomo/MatomoVisitSenderTest.php b/module/Core/test/Matomo/MatomoVisitSenderTest.php index 9655d3a4..fb04f30d 100644 --- a/module/Core/test/Matomo/MatomoVisitSenderTest.php +++ b/module/Core/test/Matomo/MatomoVisitSenderTest.php @@ -18,7 +18,6 @@ use Shlinkio\Shlink\Core\Matomo\MatomoVisitSender; use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl; use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifier; use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlCreation; -use Shlinkio\Shlink\Core\ShortUrl\Model\Validation\ShortUrlInputFilter; use Shlinkio\Shlink\Core\Visit\Entity\Visit; use Shlinkio\Shlink\Core\Visit\Entity\VisitLocation; use Shlinkio\Shlink\Core\Visit\Model\Visitor; @@ -137,10 +136,7 @@ class MatomoVisitSenderTest extends TestCase ]; yield 'non-orphan visit' => [ Visit::forValidShortUrl(ShortUrl::create( - ShortUrlCreation::fromRawData([ - ShortUrlInputFilter::LONG_URL => 'https://shlink.io', - ShortUrlInputFilter::CUSTOM_SLUG => 'bar', - ]), + new ShortUrlCreation('https://shlink.io', customSlug: 'bar'), ), Visitor::empty()), 'http://s2.test/bar', ]; diff --git a/module/Core/test/RedirectRule/ShortUrlRedirectionResolverTest.php b/module/Core/test/RedirectRule/ShortUrlRedirectionResolverTest.php index d7948be2..18bee5bf 100644 --- a/module/Core/test/RedirectRule/ShortUrlRedirectionResolverTest.php +++ b/module/Core/test/RedirectRule/ShortUrlRedirectionResolverTest.php @@ -15,7 +15,6 @@ use Shlinkio\Shlink\Core\RedirectRule\Entity\ShortUrlRedirectRule; use Shlinkio\Shlink\Core\RedirectRule\ShortUrlRedirectionResolver; use Shlinkio\Shlink\Core\RedirectRule\ShortUrlRedirectRuleServiceInterface; use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl; -use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlCreation; use const Shlinkio\Shlink\IP_ADDRESS_REQUEST_ATTRIBUTE; use const ShlinkioTest\Shlink\ANDROID_USER_AGENT; @@ -39,9 +38,7 @@ class ShortUrlRedirectionResolverTest extends TestCase RedirectCondition|null $condition, string $expectedUrl, ): void { - $shortUrl = ShortUrl::create(ShortUrlCreation::fromRawData([ - 'longUrl' => 'https://example.com/foo/bar', - ])); + $shortUrl = ShortUrl::withLongUrl('https://example.com/foo/bar'); $this->ruleService->expects($this->once())->method('rulesForShortUrl')->with($shortUrl)->willReturn( $condition !== null ? [ diff --git a/module/Core/test/ShortUrl/Entity/ShortUrlTest.php b/module/Core/test/ShortUrl/Entity/ShortUrlTest.php index 29e9d88c..d23b808a 100644 --- a/module/Core/test/ShortUrl/Entity/ShortUrlTest.php +++ b/module/Core/test/ShortUrl/Entity/ShortUrlTest.php @@ -9,12 +9,10 @@ use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\TestWith; use PHPUnit\Framework\TestCase; -use Shlinkio\Shlink\Core\Config\Options\UrlShortenerOptions; use Shlinkio\Shlink\Core\Exception\ShortCodeCannotBeRegeneratedException; use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl; use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlCreation; use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlMode; -use Shlinkio\Shlink\Core\ShortUrl\Model\Validation\ShortUrlInputFilter; use Shlinkio\Shlink\Importer\Model\ImportedShlinkUrl; use Shlinkio\Shlink\Importer\Sources\ImportSource; @@ -42,9 +40,7 @@ class ShortUrlTest extends TestCase public static function provideInvalidShortUrls(): iterable { yield 'with custom slug' => [ - ShortUrl::create( - ShortUrlCreation::fromRawData(['customSlug' => 'custom-slug', 'longUrl' => 'https://longUrl']), - ), + ShortUrl::create(new ShortUrlCreation('https://longUrl', customSlug: 'custom-slug')), 'The short code cannot be regenerated on ShortUrls where a custom slug was provided.', ]; yield 'already persisted' => [ @@ -74,12 +70,15 @@ class ShortUrlTest extends TestCase )]; } + /** + * @param int<4, max>|null $length + */ #[Test, DataProvider('provideLengths')] public function shortCodesHaveExpectedLength(int|null $length, int $expectedLength): void { - $shortUrl = ShortUrl::create(ShortUrlCreation::fromRawData( - [ShortUrlInputFilter::SHORT_CODE_LENGTH => $length, 'longUrl' => 'https://longUrl'], - )); + $shortUrl = ShortUrl::create( + new ShortUrlCreation('https://longUrl', shortCodeLength: $length ?? DEFAULT_SHORT_CODES_LENGTH), + ); self::assertEquals($expectedLength, strlen($shortUrl->getShortCode())); } @@ -98,11 +97,11 @@ class ShortUrlTest extends TestCase string $expectedPrefix, int $expectedShortCodeLength, ): void { - $shortUrl = ShortUrl::create(ShortUrlCreation::fromRawData([ - 'longUrl' => 'https://longUrl', - ShortUrlInputFilter::SHORT_CODE_LENGTH => 5, - ShortUrlInputFilter::PATH_PREFIX => $pathPrefix, - ])); + $shortUrl = ShortUrl::create(new ShortUrlCreation( + longUrl: 'https://longUrl', + pathPrefix: $pathPrefix, + shortCodeLength: 5, + )); $shortCode = $shortUrl->getShortCode(); if (strlen($expectedPrefix) > 0) { @@ -116,10 +115,7 @@ class ShortUrlTest extends TestCase { $range = range(1, 1000); // Use a "big" number to reduce false negatives $allFor = static fn (ShortUrlMode $mode): bool => every($range, static function () use ($mode): bool { - $shortUrl = ShortUrl::create(ShortUrlCreation::fromRawData( - [ShortUrlInputFilter::LONG_URL => 'https://foo'], - new UrlShortenerOptions(mode: $mode), - )); + $shortUrl = ShortUrl::create(new ShortUrlCreation('https://foo', shortUrlMode: $mode)); $shortCode = $shortUrl->getShortCode(); return $shortCode === strtolower($shortCode); diff --git a/module/Core/test/ShortUrl/Helper/ShortUrlRedirectionBuilderTest.php b/module/Core/test/ShortUrl/Helper/ShortUrlRedirectionBuilderTest.php index 6f48a836..ada5948c 100644 --- a/module/Core/test/ShortUrl/Helper/ShortUrlRedirectionBuilderTest.php +++ b/module/Core/test/ShortUrl/Helper/ShortUrlRedirectionBuilderTest.php @@ -38,10 +38,10 @@ class ShortUrlRedirectionBuilderTest extends TestCase string|null $extraPath, bool|null $forwardQuery, ): void { - $shortUrl = ShortUrl::create(ShortUrlCreation::fromRawData([ - 'longUrl' => 'https://example.com/foo/bar?some=thing', - 'forwardQuery' => $forwardQuery, - ])); + $shortUrl = ShortUrl::create(new ShortUrlCreation( + longUrl: 'https://example.com/foo/bar?some=thing', + forwardQuery: $forwardQuery ?? true, + )); $this->redirectionResolver->expects($this->once())->method('resolveLongUrl')->with( $shortUrl, $request, diff --git a/module/Core/test/ShortUrl/Helper/ShortUrlStringifierTest.php b/module/Core/test/ShortUrl/Helper/ShortUrlStringifierTest.php index 03799e10..5e6a542a 100644 --- a/module/Core/test/ShortUrl/Helper/ShortUrlStringifierTest.php +++ b/module/Core/test/ShortUrl/Helper/ShortUrlStringifierTest.php @@ -33,11 +33,7 @@ class ShortUrlStringifierTest extends TestCase public static function provideConfigAndShortUrls(): iterable { $shortUrlWithShortCode = fn (string $shortCode, string|null $domain = null) => ShortUrl::create( - ShortUrlCreation::fromRawData([ - 'longUrl' => 'https://longUrl', - 'customSlug' => $shortCode, - 'domain' => $domain, - ]), + new ShortUrlCreation('https://longUrl', customSlug: $shortCode, domain: $domain), ); yield 'no default domain' => ['', 'http', '', $shortUrlWithShortCode('foo'), 'http:/foo']; diff --git a/module/Core/test/ShortUrl/Helper/ShortUrlTitleResolutionHelperTest.php b/module/Core/test/ShortUrl/Helper/ShortUrlTitleResolutionHelperTest.php index b1f2e0d0..b987aea0 100644 --- a/module/Core/test/ShortUrl/Helper/ShortUrlTitleResolutionHelperTest.php +++ b/module/Core/test/ShortUrl/Helper/ShortUrlTitleResolutionHelperTest.php @@ -38,7 +38,7 @@ class ShortUrlTitleResolutionHelperTest extends TestCase #[Test] public function dataIsReturnedAsIsWhenResolvingTitlesIsDisabled(): void { - $data = ShortUrlCreation::fromRawData(['longUrl' => self::LONG_URL]); + $data = new ShortUrlCreation(self::LONG_URL); $this->httpClient->expects($this->never())->method('request'); $this->logger->expects($this->never())->method('warning'); @@ -50,10 +50,7 @@ class ShortUrlTitleResolutionHelperTest extends TestCase #[Test] public function dataIsReturnedAsIsWhenItAlreadyHasTitle(): void { - $data = ShortUrlCreation::fromRawData([ - 'longUrl' => self::LONG_URL, - 'title' => 'foo', - ]); + $data = new ShortUrlCreation(self::LONG_URL, title: 'foo'); $this->httpClient->expects($this->never())->method('request'); $this->logger->expects($this->never())->method('warning'); @@ -65,7 +62,7 @@ class ShortUrlTitleResolutionHelperTest extends TestCase #[Test] public function dataIsReturnedAsIsWhenFetchingFails(): void { - $data = ShortUrlCreation::fromRawData(['longUrl' => self::LONG_URL]); + $data = new ShortUrlCreation(self::LONG_URL); $this->expectRequestToBeCalled()->willThrowException(new Exception('Error')); $this->logger->expects($this->never())->method('warning'); @@ -77,7 +74,7 @@ class ShortUrlTitleResolutionHelperTest extends TestCase #[Test] public function dataIsReturnedAsIsWhenResponseIsNotHtml(): void { - $data = ShortUrlCreation::fromRawData(['longUrl' => self::LONG_URL]); + $data = new ShortUrlCreation(self::LONG_URL); $this->expectRequestToBeCalled()->willReturn(new JsonResponse(['foo' => 'bar'])); $this->logger->expects($this->never())->method('warning'); @@ -89,7 +86,7 @@ class ShortUrlTitleResolutionHelperTest extends TestCase #[Test] public function dataIsReturnedAsIsWhenTitleCannotBeResolvedFromResponse(): void { - $data = ShortUrlCreation::fromRawData(['longUrl' => self::LONG_URL]); + $data = new ShortUrlCreation(self::LONG_URL); $this->expectRequestToBeCalled()->willReturn($this->respWithoutTitle()); $this->logger->expects($this->never())->method('warning'); @@ -113,7 +110,7 @@ class ShortUrlTitleResolutionHelperTest extends TestCase $this->logger->expects($this->never())->method('warning'); } - $data = ShortUrlCreation::fromRawData(['longUrl' => self::LONG_URL]); + $data = new ShortUrlCreation(self::LONG_URL); $result = $this->helper(autoResolveTitles: true, iconvEnabled: true)->processTitle($data); self::assertNotSame($data, $result); @@ -125,7 +122,7 @@ class ShortUrlTitleResolutionHelperTest extends TestCase { $this->expectRequestToBeCalled()->willReturn($this->respWithTitle('text/html')); - $data = ShortUrlCreation::fromRawData(['longUrl' => self::LONG_URL]); + $data = new ShortUrlCreation(self::LONG_URL); $result = $this->helper(autoResolveTitles: true, iconvEnabled: true)->processTitle($data); self::assertSame($data, $result); @@ -144,7 +141,7 @@ class ShortUrlTitleResolutionHelperTest extends TestCase extraContent: $extraContent, )); - $data = ShortUrlCreation::fromRawData(['longUrl' => self::LONG_URL]); + $data = new ShortUrlCreation(self::LONG_URL); $result = $this->helper(autoResolveTitles: true, iconvEnabled: true)->processTitle($data); self::assertNotSame($data, $result); @@ -180,7 +177,7 @@ class ShortUrlTitleResolutionHelperTest extends TestCase }, )); - $data = ShortUrlCreation::fromRawData(['longUrl' => self::LONG_URL]); + $data = new ShortUrlCreation(self::LONG_URL); $result = $this->helper(autoResolveTitles: true, iconvEnabled: $iconvEnabled)->processTitle($data); self::assertNotSame($data, $result); diff --git a/module/Core/test/ShortUrl/Model/ShortUrlCreationTest.php b/module/Core/test/ShortUrl/Model/ShortUrlCreationTest.php index ed9c6459..25c0ac3b 100644 --- a/module/Core/test/ShortUrl/Model/ShortUrlCreationTest.php +++ b/module/Core/test/ShortUrl/Model/ShortUrlCreationTest.php @@ -4,106 +4,30 @@ declare(strict_types=1); namespace ShlinkioTest\Shlink\Core\ShortUrl\Model; -use Cake\Chronos\Chronos; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; -use Shlinkio\Shlink\Core\Config\Options\UrlShortenerOptions; -use Shlinkio\Shlink\Core\Exception\ValidationException; use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlCreation; use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlMode; -use Shlinkio\Shlink\Core\ShortUrl\Model\Validation\ShortUrlInputFilter; -use stdClass; - -use function str_pad; - -use const STR_PAD_BOTH; class ShortUrlCreationTest extends TestCase { - #[Test, DataProvider('provideInvalidData')] - public function exceptionIsThrownIfProvidedDataIsInvalid(array $data): void - { - $this->expectException(ValidationException::class); - ShortUrlCreation::fromRawData($data); - } - - public static function provideInvalidData(): iterable - { - yield [[]]; - yield [[ - ShortUrlInputFilter::LONG_URL => 'https://foo', - ShortUrlInputFilter::VALID_SINCE => '', - ShortUrlInputFilter::VALID_UNTIL => '', - ShortUrlInputFilter::CUSTOM_SLUG => 'foobar', - ShortUrlInputFilter::MAX_VISITS => 'invalid', - ]]; - yield [[ - ShortUrlInputFilter::LONG_URL => 'https://foo', - ShortUrlInputFilter::VALID_SINCE => '2017', - ShortUrlInputFilter::MAX_VISITS => 5, - ]]; - yield [[ - ShortUrlInputFilter::LONG_URL => 'https://foo', - ShortUrlInputFilter::VALID_SINCE => new stdClass(), - ShortUrlInputFilter::VALID_UNTIL => 'foo', - ]]; - yield [[ - ShortUrlInputFilter::LONG_URL => 'https://foo', - ShortUrlInputFilter::VALID_UNTIL => 500, - ShortUrlInputFilter::DOMAIN => 4, - ]]; - yield [[ - ShortUrlInputFilter::LONG_URL => 'https://foo', - ShortUrlInputFilter::SHORT_CODE_LENGTH => 3, - ]]; - yield [[ - ShortUrlInputFilter::LONG_URL => 'https://foo', - ShortUrlInputFilter::CUSTOM_SLUG => '', - ]]; - yield [[ - ShortUrlInputFilter::LONG_URL => 'https://foo', - ShortUrlInputFilter::CUSTOM_SLUG => 'foo?some=param', - ]]; - yield [[ - ShortUrlInputFilter::LONG_URL => 'https://foo', - ShortUrlInputFilter::CUSTOM_SLUG => ' ', - ]]; - yield [[ - ShortUrlInputFilter::LONG_URL => [], - ]]; - yield [[ - ShortUrlInputFilter::LONG_URL => null, - ]]; - yield [[ - ShortUrlInputFilter::LONG_URL => 'missing_schema', - ]]; - } - #[Test, DataProvider('provideCustomSlugs')] - public function properlyCreatedInstanceReturnsValues( + public function properlyCreatesInstancesWithCustomSlug( string $customSlug, string $expectedSlug, bool $multiSegmentEnabled = false, ShortUrlMode $shortUrlMode = ShortUrlMode::STRICT, ): void { - $creation = ShortUrlCreation::fromRawData([ - 'validSince' => Chronos::parse('2015-01-01')->toAtomString(), - 'customSlug' => $customSlug, - 'longUrl' => 'https://longUrl', - ], new UrlShortenerOptions(multiSegmentSlugsEnabled: $multiSegmentEnabled, mode: $shortUrlMode)); - - self::assertTrue($creation->hasValidSince()); - self::assertEquals(Chronos::parse('2015-01-01'), $creation->validSince); - - self::assertFalse($creation->hasValidUntil()); - self::assertNull($creation->validUntil); + $creation = new ShortUrlCreation( + longUrl: 'https://longUrl', + shortUrlMode: $shortUrlMode, + multiSegmentSlugsEnabled: $multiSegmentEnabled, + customSlug: $customSlug, + ); self::assertTrue($creation->hasCustomSlug()); self::assertEquals($expectedSlug, $creation->customSlug); - - self::assertFalse($creation->hasMaxVisits()); - self::assertNull($creation->maxVisits); } public static function provideCustomSlugs(): iterable @@ -130,61 +54,4 @@ class ShortUrlCreationTest extends TestCase yield ['谷歌', '谷歌']; yield ['гугл', 'гугл']; } - - #[Test, DataProvider('provideValidLongUrls')] - public function supportsDifferentTypesOfSchemas(string $longUrl): void - { - $creation = ShortUrlCreation::fromRawData(['longUrl' => $longUrl]); - self::assertEquals($longUrl, $creation->longUrl); - } - - public static function provideValidLongUrls(): iterable - { - yield 'mailto' => ['mailto:foo@example.com']; - yield 'file' => ['file:///foo/bar']; - yield 'https' => ['https://example.com']; - yield 'deeplink' => ['shlink://some/path']; - } - - #[Test, DataProvider('provideTitles')] - public function titleIsCroppedIfTooLong(string|null $title, string|null $expectedTitle): void - { - $creation = ShortUrlCreation::fromRawData([ - 'title' => $title, - 'longUrl' => 'https://longUrl', - ]); - - self::assertEquals($expectedTitle, $creation->title); - } - - public static function provideTitles(): iterable - { - yield [null, null]; - yield ['foo', 'foo']; - yield [str_pad('bar', 600, ' ', STR_PAD_BOTH), 'bar']; - yield [str_pad('', 511, 'a'), str_pad('', 511, 'a')]; - yield [str_pad('', 512, 'b'), str_pad('', 512, 'b')]; - yield [str_pad('', 513, 'c'), str_pad('', 512, 'c')]; - yield [str_pad('', 600, 'd'), str_pad('', 512, 'd')]; - yield [str_pad('', 800, 'e'), str_pad('', 512, 'e')]; - } - - #[Test, DataProvider('provideDomains')] - public function emptyDomainIsDiscarded(string|null $domain, string|null $expectedDomain): void - { - $creation = ShortUrlCreation::fromRawData([ - 'domain' => $domain, - 'longUrl' => 'https://longUrl', - ]); - - self::assertSame($expectedDomain, $creation->domain); - } - - public static function provideDomains(): iterable - { - yield 'null domain' => [null, null]; - yield 'empty domain' => ['', null]; - yield 'trimmable domain' => [' ', null]; - yield 'valid domain' => ['s.test', 's.test']; - } } diff --git a/module/Core/test/ShortUrl/ShortUrlResolverTest.php b/module/Core/test/ShortUrl/ShortUrlResolverTest.php index d565a352..19927e17 100644 --- a/module/Core/test/ShortUrl/ShortUrlResolverTest.php +++ b/module/Core/test/ShortUrl/ShortUrlResolverTest.php @@ -124,9 +124,7 @@ class ShortUrlResolverTest extends TestCase $now = Chronos::now(); yield 'maxVisits reached' => [(function () { - $shortUrl = ShortUrl::create( - ShortUrlCreation::fromRawData(['maxVisits' => 3, 'longUrl' => 'https://longUrl']), - ); + $shortUrl = ShortUrl::create(new ShortUrlCreation('https://longUrl', maxVisits: 3)); $shortUrl->setVisits(new ArrayCollection(array_map( fn () => Visit::forValidShortUrl($shortUrl, Visitor::empty()), range(0, 4), @@ -134,18 +132,20 @@ class ShortUrlResolverTest extends TestCase return $shortUrl; })()]; - yield 'future validSince' => [ShortUrl::create(ShortUrlCreation::fromRawData( - ['validSince' => $now->addMonths(1)->toAtomString(), 'longUrl' => 'https://longUrl'], + yield 'future validSince' => [ShortUrl::create(new ShortUrlCreation( + longUrl: 'https://longUrl', + validSince: $now->addMonths(1), ))]; - yield 'past validUntil' => [ShortUrl::create(ShortUrlCreation::fromRawData( - ['validUntil' => $now->subMonths(1)->toAtomString(), 'longUrl' => 'https://longUrl'], + yield 'past validUntil' => [ShortUrl::create(new ShortUrlCreation( + longUrl: 'https://longUrl', + validUntil: $now->subMonths(1), ))]; yield 'mixed' => [(function () use ($now) { - $shortUrl = ShortUrl::create(ShortUrlCreation::fromRawData([ - 'maxVisits' => 3, - 'validUntil' => $now->subMonths(1)->toAtomString(), - 'longUrl' => 'https://longUrl', - ])); + $shortUrl = ShortUrl::create(new ShortUrlCreation( + longUrl: 'https://longUrl', + validUntil: $now->subMonths(1), + maxVisits: 3, + )); $shortUrl->setVisits(new ArrayCollection(array_map( fn () => Visit::forValidShortUrl($shortUrl, Visitor::empty()), range(0, 4), diff --git a/module/Core/test/ShortUrl/Transformer/ShortUrlDataTransformerTest.php b/module/Core/test/ShortUrl/Transformer/ShortUrlDataTransformerTest.php index 9ff01475..beb91cd2 100644 --- a/module/Core/test/ShortUrl/Transformer/ShortUrlDataTransformerTest.php +++ b/module/Core/test/ShortUrl/Transformer/ShortUrlDataTransformerTest.php @@ -42,18 +42,16 @@ class ShortUrlDataTransformerTest extends TestCase 'validUntil' => null, 'maxVisits' => null, ]]; - yield 'max visits only' => [ShortUrl::create(ShortUrlCreation::fromRawData([ - 'maxVisits' => $maxVisits, - 'longUrl' => 'https://longUrl', - ])), [ + yield 'max visits only' => [ShortUrl::create(new ShortUrlCreation( + longUrl: 'https://longUrl', + maxVisits: $maxVisits, + )), [ 'validSince' => null, 'validUntil' => null, 'maxVisits' => $maxVisits, ]]; yield 'max visits and valid since' => [ - ShortUrl::create(ShortUrlCreation::fromRawData( - ['validSince' => $now, 'maxVisits' => $maxVisits, 'longUrl' => 'https://longUrl'], - )), + ShortUrl::create(new ShortUrlCreation('https://longUrl', validSince: $now, maxVisits: $maxVisits)), [ 'validSince' => $now->toAtomString(), 'validUntil' => null, @@ -61,9 +59,7 @@ class ShortUrlDataTransformerTest extends TestCase ], ]; yield 'both dates' => [ - ShortUrl::create(ShortUrlCreation::fromRawData( - ['validSince' => $now, 'validUntil' => $now->subDays(10), 'longUrl' => 'https://longUrl'], - )), + ShortUrl::create(new ShortUrlCreation('https://longUrl', validSince: $now, validUntil: $now->subDays(10))), [ 'validSince' => $now->toAtomString(), 'validUntil' => $now->subDays(10)->toAtomString(), @@ -71,12 +67,12 @@ class ShortUrlDataTransformerTest extends TestCase ], ]; yield 'everything' => [ - ShortUrl::create(ShortUrlCreation::fromRawData([ - 'validSince' => $now, - 'validUntil' => $now->subDays(5), - 'maxVisits' => $maxVisits, - 'longUrl' => 'https://longUrl', - ])), + ShortUrl::create(new ShortUrlCreation( + longUrl: 'https://longUrl', + validSince: $now, + validUntil: $now->subDays(5), + maxVisits: $maxVisits, + )), [ 'validSince' => $now->toAtomString(), 'validUntil' => $now->subDays(5)->toAtomString(), @@ -88,10 +84,10 @@ class ShortUrlDataTransformerTest extends TestCase #[Test] public function properTagsAreReturned(): void { - ['tags' => $tags] = $this->transformer->transform(ShortUrl::create(ShortUrlCreation::fromRawData([ - 'longUrl' => 'https://longUrl', - 'tags' => ['foo', 'bar', 'baz'], - ]))); + ['tags' => $tags] = $this->transformer->transform(ShortUrl::create(new ShortUrlCreation( + longUrl: 'https://longUrl', + tags: ['foo', 'bar', 'baz'], + ))); self::assertEquals(['foo', 'bar', 'baz'], $tags); } } diff --git a/module/Core/test/ShortUrl/UrlShortenerTest.php b/module/Core/test/ShortUrl/UrlShortenerTest.php index 8e78ff18..23041c97 100644 --- a/module/Core/test/ShortUrl/UrlShortenerTest.php +++ b/module/Core/test/ShortUrl/UrlShortenerTest.php @@ -57,7 +57,7 @@ class UrlShortenerTest extends TestCase public function urlIsProperlyShortened(bool $expectDispatchError, callable $dispatchBehavior): void { $longUrl = 'http://foobar.com/12345/hello?foo=bar'; - $meta = ShortUrlCreation::fromRawData(['longUrl' => $longUrl]); + $meta = new ShortUrlCreation($longUrl); $this->titleResolutionHelper->expects($this->once())->method('processTitle')->with( $meta, )->willReturnArgument(0); @@ -86,9 +86,7 @@ class UrlShortenerTest extends TestCase #[Test] public function exceptionIsThrownWhenNonUniqueSlugIsProvided(): void { - $meta = ShortUrlCreation::fromRawData( - ['customSlug' => 'custom-slug', 'longUrl' => 'http://foobar.com/12345/hello?foo=bar'], - ); + $meta = new ShortUrlCreation(longUrl: 'http://foobar.com/12345/hello?foo=bar', customSlug: 'custom-slug'); $this->shortCodeHelper->expects($this->once())->method('ensureShortCodeUniqueness')->willReturn(false); $this->titleResolutionHelper->expects($this->once())->method('processTitle')->with( @@ -116,54 +114,42 @@ class UrlShortenerTest extends TestCase { $url = 'http://foo.com'; - yield [ShortUrlCreation::fromRawData(['findIfExists' => true, 'longUrl' => $url]), ShortUrl::withLongUrl( - $url, - )]; - yield [ShortUrlCreation::fromRawData( - ['findIfExists' => true, 'customSlug' => 'foo', 'longUrl' => $url], - ), ShortUrl::withLongUrl($url)]; + yield [new ShortUrlCreation($url, findIfExists: true), ShortUrl::withLongUrl($url)]; + yield [new ShortUrlCreation($url, customSlug: 'foo', findIfExists: true), ShortUrl::withLongUrl($url)]; yield [ - ShortUrlCreation::fromRawData(['findIfExists' => true, 'longUrl' => $url, 'tags' => ['foo', 'bar']]), - ShortUrl::create(ShortUrlCreation::fromRawData(['longUrl' => $url, 'tags' => ['foo', 'bar']])), + new ShortUrlCreation($url, findIfExists: true, tags: ['foo', 'bar']), + ShortUrl::create(new ShortUrlCreation($url, tags: ['foo', 'bar'])), ]; yield [ - ShortUrlCreation::fromRawData(['findIfExists' => true, 'maxVisits' => 3, 'longUrl' => $url]), - ShortUrl::create(ShortUrlCreation::fromRawData(['maxVisits' => 3, 'longUrl' => $url])), + new ShortUrlCreation($url, maxVisits: 3, findIfExists: true), + ShortUrl::create(new ShortUrlCreation($url, maxVisits: 3)), ]; yield [ - ShortUrlCreation::fromRawData( - ['findIfExists' => true, 'validSince' => Chronos::parse('2017-01-01'), 'longUrl' => $url], + new ShortUrlCreation($url, validSince: Chronos::parse('2017-01-01'), findIfExists: true), + ShortUrl::create(new ShortUrlCreation($url, validSince: Chronos::parse('2017-01-01'))), + ]; + yield [ + new ShortUrlCreation($url, validUntil: Chronos::parse('2017-01-01'), findIfExists: true), + ShortUrl::create(new ShortUrlCreation($url, validUntil: Chronos::parse('2017-01-01'))), + ]; + yield [ + new ShortUrlCreation($url, findIfExists: true, domain: 'example.com'), + ShortUrl::create(new ShortUrlCreation($url, domain: 'example.com')), + ]; + yield [ + new ShortUrlCreation( + longUrl: $url, + validUntil: Chronos::parse('2017-01-01'), + maxVisits: 4, + findIfExists: true, + tags: ['baz', 'foo', 'bar'], ), - ShortUrl::create( - ShortUrlCreation::fromRawData(['validSince' => Chronos::parse('2017-01-01'), 'longUrl' => $url]), - ), - ]; - yield [ - ShortUrlCreation::fromRawData( - ['findIfExists' => true, 'validUntil' => Chronos::parse('2017-01-01'), 'longUrl' => $url], - ), - ShortUrl::create( - ShortUrlCreation::fromRawData(['validUntil' => Chronos::parse('2017-01-01'), 'longUrl' => $url]), - ), - ]; - yield [ - ShortUrlCreation::fromRawData(['findIfExists' => true, 'domain' => 'example.com', 'longUrl' => $url]), - ShortUrl::create(ShortUrlCreation::fromRawData(['domain' => 'example.com', 'longUrl' => $url])), - ]; - yield [ - ShortUrlCreation::fromRawData([ - 'findIfExists' => true, - 'validUntil' => Chronos::parse('2017-01-01'), - 'maxVisits' => 4, - 'longUrl' => $url, - 'tags' => ['baz', 'foo', 'bar'], - ]), - ShortUrl::create(ShortUrlCreation::fromRawData([ - 'validUntil' => Chronos::parse('2017-01-01'), - 'maxVisits' => 4, - 'longUrl' => $url, - 'tags' => ['foo', 'bar', 'baz'], - ])), + ShortUrl::create(new ShortUrlCreation( + longUrl: $url, + validUntil: Chronos::parse('2017-01-01'), + maxVisits: 4, + tags: ['foo', 'bar', 'baz'], + )), ]; } } diff --git a/module/Rest/config/dependencies.config.php b/module/Rest/config/dependencies.config.php index ec0335d1..c1145e14 100644 --- a/module/Rest/config/dependencies.config.php +++ b/module/Rest/config/dependencies.config.php @@ -74,11 +74,13 @@ return [ ShortUrl\UrlShortener::class, ShortUrlDataTransformer::class, Config\Options\UrlShortenerOptions::class, + TreeMapper::class, ], Action\ShortUrl\SingleStepCreateShortUrlAction::class => [ ShortUrl\UrlShortener::class, ShortUrlDataTransformer::class, Config\Options\UrlShortenerOptions::class, + TreeMapper::class, ], Action\ShortUrl\EditShortUrlAction::class => [ShortUrl\ShortUrlService::class, ShortUrlDataTransformer::class], Action\ShortUrl\DeleteShortUrlAction::class => [ShortUrl\DeleteShortUrlService::class], diff --git a/module/Rest/src/Action/ShortUrl/AbstractCreateShortUrlAction.php b/module/Rest/src/Action/ShortUrl/AbstractCreateShortUrlAction.php index 75d5480b..50b67eb1 100644 --- a/module/Rest/src/Action/ShortUrl/AbstractCreateShortUrlAction.php +++ b/module/Rest/src/Action/ShortUrl/AbstractCreateShortUrlAction.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Shlinkio\Shlink\Rest\Action\ShortUrl; +use CuyZ\Valinor\Mapper\TreeMapper; use Laminas\Diactoros\Response\JsonResponse; use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ServerRequestInterface as Request; @@ -19,7 +20,8 @@ abstract class AbstractCreateShortUrlAction extends AbstractRestAction public function __construct( private readonly UrlShortenerInterface $urlShortener, private readonly ShortUrlDataTransformerInterface $transformer, - protected readonly UrlShortenerOptions $urlShortenerOptions, + private readonly UrlShortenerOptions $urlShortenerOptions, + private readonly TreeMapper $treeMapper, ) { } @@ -31,6 +33,15 @@ abstract class AbstractCreateShortUrlAction extends AbstractRestAction return new JsonResponse($this->transformer->transform($result->shortUrl)); } + protected function mapShortUrlCreation(array $payload): ShortUrlCreation + { + return $this->treeMapper->map(ShortUrlCreation::class, [ + ...$payload, + 'shortUrlMode' => $this->urlShortenerOptions->mode, + 'multiSegmentSlugsEnabled' => $this->urlShortenerOptions->multiSegmentSlugsEnabled, + ]); + } + /** * @throws ValidationException */ diff --git a/module/Rest/src/Action/ShortUrl/CreateShortUrlAction.php b/module/Rest/src/Action/ShortUrl/CreateShortUrlAction.php index 6f5e291c..bccd1a32 100644 --- a/module/Rest/src/Action/ShortUrl/CreateShortUrlAction.php +++ b/module/Rest/src/Action/ShortUrl/CreateShortUrlAction.php @@ -5,9 +5,7 @@ declare(strict_types=1); namespace Shlinkio\Shlink\Rest\Action\ShortUrl; use Psr\Http\Message\ServerRequestInterface as Request; -use Shlinkio\Shlink\Core\Exception\ValidationException; use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlCreation; -use Shlinkio\Shlink\Core\ShortUrl\Model\Validation\ShortUrlInputFilter; use Shlinkio\Shlink\Rest\Middleware\AuthenticationMiddleware; class CreateShortUrlAction extends AbstractCreateShortUrlAction @@ -15,14 +13,13 @@ class CreateShortUrlAction extends AbstractCreateShortUrlAction protected const string ROUTE_PATH = '/short-urls'; protected const array ROUTE_ALLOWED_METHODS = [self::METHOD_POST]; - /** - * @throws ValidationException - */ protected function buildShortUrlData(Request $request): ShortUrlCreation { - $payload = (array) $request->getParsedBody(); - $payload[ShortUrlInputFilter::API_KEY] = AuthenticationMiddleware::apiKeyFromRequest($request); + $body = (array) $request->getParsedBody(); - return ShortUrlCreation::fromRawData($payload, $this->urlShortenerOptions); + return $this->mapShortUrlCreation([ + ...$body, + 'apiKey' => AuthenticationMiddleware::apiKeyFromRequest($request), + ]); } } diff --git a/module/Rest/src/Action/ShortUrl/SingleStepCreateShortUrlAction.php b/module/Rest/src/Action/ShortUrl/SingleStepCreateShortUrlAction.php index 039b82d4..f6b1094c 100644 --- a/module/Rest/src/Action/ShortUrl/SingleStepCreateShortUrlAction.php +++ b/module/Rest/src/Action/ShortUrl/SingleStepCreateShortUrlAction.php @@ -6,8 +6,8 @@ namespace Shlinkio\Shlink\Rest\Action\ShortUrl; use Psr\Http\Message\ServerRequestInterface as Request; use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlCreation; -use Shlinkio\Shlink\Core\ShortUrl\Model\Validation\ShortUrlInputFilter; use Shlinkio\Shlink\Rest\Middleware\AuthenticationMiddleware; +use Shlinkio\Shlink\Rest\Middleware\ShortUrl\OverrideDomainMiddleware; class SingleStepCreateShortUrlAction extends AbstractCreateShortUrlAction { @@ -20,11 +20,11 @@ class SingleStepCreateShortUrlAction extends AbstractCreateShortUrlAction $longUrl = $query['longUrl'] ?? null; $apiKey = AuthenticationMiddleware::apiKeyFromRequest($request); - return ShortUrlCreation::fromRawData([ - ShortUrlInputFilter::LONG_URL => $longUrl, - ShortUrlInputFilter::API_KEY => $apiKey, + return $this->mapShortUrlCreation([ + 'longUrl' => $longUrl, + 'apiKey' => $apiKey, // This will usually be null, unless this API key enforces one specific domain - ShortUrlInputFilter::DOMAIN => $request->getAttribute(ShortUrlInputFilter::DOMAIN), - ], $this->urlShortenerOptions); + 'domain' => OverrideDomainMiddleware::domainFromRequest($request), + ]); } } diff --git a/module/Rest/src/Middleware/ShortUrl/OverrideDomainMiddleware.php b/module/Rest/src/Middleware/ShortUrl/OverrideDomainMiddleware.php index 8a88e340..534ed20a 100644 --- a/module/Rest/src/Middleware/ShortUrl/OverrideDomainMiddleware.php +++ b/module/Rest/src/Middleware/ShortUrl/OverrideDomainMiddleware.php @@ -10,13 +10,14 @@ use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; use Shlinkio\Shlink\Core\Domain\DomainServiceInterface; -use Shlinkio\Shlink\Core\ShortUrl\Model\Validation\ShortUrlInputFilter; use Shlinkio\Shlink\Rest\ApiKey\Role; use Shlinkio\Shlink\Rest\Middleware\AuthenticationMiddleware; class OverrideDomainMiddleware implements MiddlewareInterface { - public function __construct(private DomainServiceInterface $domainService) + private const string REQUEST_ATTRIBUTE = 'domain'; + + public function __construct(private readonly DomainServiceInterface $domainService) { } @@ -34,11 +35,16 @@ class OverrideDomainMiddleware implements MiddlewareInterface if ($requestMethod === RequestMethodInterface::METHOD_POST) { /** @var array $payload */ $payload = $request->getParsedBody(); - $payload[ShortUrlInputFilter::DOMAIN] = $domain->authority; + $payload[self::REQUEST_ATTRIBUTE] = $domain->authority; return $handler->handle($request->withParsedBody($payload)); } - return $handler->handle($request->withAttribute(ShortUrlInputFilter::DOMAIN, $domain->authority)); + return $handler->handle($request->withAttribute(self::REQUEST_ATTRIBUTE, $domain->authority)); + } + + public static function domainFromRequest(ServerRequestInterface $request): string|null + { + return $request->getAttribute(self::REQUEST_ATTRIBUTE); } } diff --git a/module/Rest/test-api/Action/CreateShortUrlTest.php b/module/Rest/test-api/Action/CreateShortUrlTest.php index 212b545c..8e041d2d 100644 --- a/module/Rest/test-api/Action/CreateShortUrlTest.php +++ b/module/Rest/test-api/Action/CreateShortUrlTest.php @@ -14,6 +14,9 @@ use Shlinkio\Shlink\TestUtils\ApiTest\ApiTestCase; use function array_map; use function range; use function sprintf; +use function str_pad; + +use const STR_PAD_BOTH; class CreateShortUrlTest extends ApiTestCase { @@ -29,6 +32,23 @@ class CreateShortUrlTest extends ApiTestCase } } + #[Test, DataProvider('provideValidLongUrls')] + public function lonUrlSupportsDifferentTypesOfSchemas(string $longUrl): void + { + [$statusCode, $payload] = $this->createShortUrl(['longUrl' => $longUrl]); + + self::assertEquals(self::STATUS_OK, $statusCode); + self::assertEquals($longUrl, $payload['longUrl']); + } + + public static function provideValidLongUrls(): iterable + { + yield 'mailto' => ['mailto:foo@example.com']; + yield 'file' => ['file:///foo/bar']; + yield 'https' => ['https://example.com']; + yield 'deeplink' => ['shlink://some/path']; + } + #[Test] public function createsNewShortUrlWithCustomSlug(): void { @@ -228,7 +248,7 @@ class CreateShortUrlTest extends ApiTestCase } #[Test, DataProvider('provideInvalidArgumentApiVersions')] - public function failsToCreateShortUrlWithoutLongUrl(array $payload, string $version, string $expectedType): void + public function failsToCreateShortUrlWithoutLongUrl(array $payload, string $version): void { $resp = $this->callApiWithKey( self::METHOD_POST, @@ -239,19 +259,19 @@ class CreateShortUrlTest extends ApiTestCase self::assertEquals(self::STATUS_BAD_REQUEST, $resp->getStatusCode()); self::assertEquals(self::STATUS_BAD_REQUEST, $payload['status']); - self::assertEquals($expectedType, $payload['type']); + self::assertEquals('https://shlink.io/api/error/invalid-data', $payload['type']); self::assertEquals('Provided data is not valid', $payload['detail']); self::assertEquals('Invalid data', $payload['title']); } public static function provideInvalidArgumentApiVersions(): iterable { - yield 'missing long url v2' => [[], '2', 'https://shlink.io/api/error/invalid-data']; - yield 'missing long url v3' => [[], '3', 'https://shlink.io/api/error/invalid-data']; - yield 'empty long url v2' => [['longUrl' => null], '2', 'https://shlink.io/api/error/invalid-data']; - 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 'missing long url v2' => [[], '2']; + yield 'missing long url v3' => [[], '3']; + yield 'empty long url v2' => [['longUrl' => null], '2']; + yield 'empty long url v3' => [['longUrl' => ' '], '3']; + yield 'missing url schema v2' => [['longUrl' => 'foo.com'], '2']; + yield 'missing url schema v3' => [['longUrl' => 'foo.com'], '3']; } #[Test] @@ -314,6 +334,26 @@ class CreateShortUrlTest extends ApiTestCase self::assertNull($payload['title']); } + #[Test, DataProvider('provideTitles')] + public function titleIsCroppedIfTooLong(string $title, string $expectedTitle): void + { + [$statusCode, ['title' => $returnedTitle]] = $this->createShortUrl(['title' => $title]); + + self::assertEquals(self::STATUS_OK, $statusCode); + self::assertEquals($expectedTitle, $returnedTitle); + } + + public static function provideTitles(): iterable + { + yield ['foo', 'foo']; + yield [str_pad('bar', 600, ' ', STR_PAD_BOTH), 'bar']; + yield [str_pad('', 511, 'a'), str_pad('', 511, 'a')]; + yield [str_pad('', 512, 'b'), str_pad('', 512, 'b')]; + yield [str_pad('', 513, 'c'), str_pad('', 512, 'c')]; + yield [str_pad('', 600, 'd'), str_pad('', 512, 'd')]; + yield [str_pad('', 800, 'e'), str_pad('', 512, 'e')]; + } + #[Test] #[TestWith([null])] #[TestWith(['my-custom-slug'])] @@ -329,6 +369,23 @@ class CreateShortUrlTest extends ApiTestCase self::assertStringStartsWith('foo-b--ar-baz', $payload['shortCode']); } + + + #[Test] + #[TestWith(['localhost:80000'])] + #[TestWith(['127.0.0.1'])] + #[TestWith(['???/&%$&'])] + public function failsToCreateShortUrlWithInvalidDomain(string $domain): void + { + [$statusCode, $payload] = $this->createShortUrl(['domain' => $domain]); + + self::assertEquals(self::STATUS_BAD_REQUEST, $statusCode); + self::assertEquals(self::STATUS_BAD_REQUEST, $payload['status']); + self::assertEquals('https://shlink.io/api/error/invalid-data', $payload['type']); + self::assertEquals('Provided data is not valid', $payload['detail']); + self::assertEquals('Invalid data', $payload['title']); + } + /** * @return array{int, array} */ diff --git a/module/Rest/test-api/Fixtures/ShortUrlsFixture.php b/module/Rest/test-api/Fixtures/ShortUrlsFixture.php index aec8098f..3edb098c 100644 --- a/module/Rest/test-api/Fixtures/ShortUrlsFixture.php +++ b/module/Rest/test-api/Fixtures/ShortUrlsFixture.php @@ -29,61 +29,61 @@ class ShortUrlsFixture extends AbstractFixture implements DependentFixtureInterf $authorApiKey = $this->getReference('author_api_key'); $abcShortUrl = $this->setShortUrlDate( - ShortUrl::create(ShortUrlCreation::fromRawData([ - 'customSlug' => 'abc123', - 'apiKey' => $authorApiKey, - 'longUrl' => 'https://shlink.io', - 'tags' => ['foo'], - 'title' => 'My cool title', - 'crawlable' => true, - 'maxVisits' => 2, - ]), $relationResolver), + ShortUrl::create(new ShortUrlCreation( + longUrl: 'https://shlink.io', + customSlug: 'abc123', + maxVisits: 2, + apiKey: $authorApiKey, + tags: ['foo'], + title: 'My cool title', + crawlable: true, + ), $relationResolver), '2018-05-01', ); $manager->persist($abcShortUrl); - $defShortUrl = $this->setShortUrlDate(ShortUrl::create(ShortUrlCreation::fromRawData([ - 'validSince' => Chronos::parse('2020-05-01'), - 'customSlug' => 'def456', - 'apiKey' => $authorApiKey, - 'longUrl' => + $defShortUrl = $this->setShortUrlDate(ShortUrl::create(new ShortUrlCreation( + longUrl: 'https://blog.alejandrocelaya.com/2017/12/09/acmailer-7-0-the-most-important-release-in-a-long-time/', - 'tags' => ['foo', 'bar'], - ]), $relationResolver), '2019-01-01 00:00:10'); + validSince: Chronos::parse('2020-05-01'), + customSlug: 'def456', + apiKey: $authorApiKey, + tags: ['foo', 'bar'], + ), $relationResolver), '2019-01-01 00:00:10'); $manager->persist($defShortUrl); - $customShortUrl = $this->setShortUrlDate(ShortUrl::create(ShortUrlCreation::fromRawData([ - 'customSlug' => 'custom', - 'maxVisits' => 2, - 'apiKey' => $authorApiKey, - 'longUrl' => 'https://shlink.io', - 'crawlable' => true, - 'forwardQuery' => false, - ])), '2019-01-01 00:00:20'); + $customShortUrl = $this->setShortUrlDate(ShortUrl::create(new ShortUrlCreation( + longUrl: 'https://shlink.io', + customSlug: 'custom', + maxVisits: 2, + apiKey: $authorApiKey, + crawlable: true, + forwardQuery: false, + )), '2019-01-01 00:00:20'); $manager->persist($customShortUrl); $ghiShortUrl = $this->setShortUrlDate( - ShortUrl::create(ShortUrlCreation::fromRawData([ - 'customSlug' => 'ghi789', - 'longUrl' => 'https://shlink.io/documentation/', - 'validUntil' => Chronos::parse('2020-05-01'), // In the past - ])), + ShortUrl::create(new ShortUrlCreation( + longUrl: 'https://shlink.io/documentation/', + validUntil: Chronos::parse('2020-05-01'), // In the past + customSlug: 'ghi789', + )), '2018-05-01', ); $manager->persist($ghiShortUrl); - $withDomainDuplicatingShortCode = $this->setShortUrlDate(ShortUrl::create(ShortUrlCreation::fromRawData([ - 'domain' => 'example.com', - 'customSlug' => 'ghi789', - 'longUrl' => 'https://blog.alejandrocelaya.com/2019/04/27/considerations-to-properly-use-open-' + $withDomainDuplicatingShortCode = $this->setShortUrlDate(ShortUrl::create(new ShortUrlCreation( + longUrl: 'https://blog.alejandrocelaya.com/2019/04/27/considerations-to-properly-use-open-' . 'source-software-projects/', - 'tags' => ['foo'], - ]), $relationResolver), '2019-01-01 00:00:30'); + customSlug: 'ghi789', + domain: 'example.com', + tags: ['foo'], + ), $relationResolver), '2019-01-01 00:00:30'); $manager->persist($withDomainDuplicatingShortCode); - $withDomainAndSlugShortUrl = $this->setShortUrlDate(ShortUrl::create(ShortUrlCreation::fromRawData( - ['domain' => 'some-domain.com', 'customSlug' => 'custom-with-domain', 'longUrl' => 'https://google.com'], - )), '2018-10-20'); + $withDomainAndSlugShortUrl = $this->setShortUrlDate(ShortUrl::create( + new ShortUrlCreation('https://google.com', customSlug: 'custom-with-domain', domain: 'some-domain.com'), + ), '2018-10-20'); $manager->persist($withDomainAndSlugShortUrl); $manager->flush(); diff --git a/module/Rest/test/Action/ShortUrl/CreateShortUrlActionTest.php b/module/Rest/test/Action/ShortUrl/CreateShortUrlActionTest.php index 06cd5554..ca7ce6dc 100644 --- a/module/Rest/test/Action/ShortUrl/CreateShortUrlActionTest.php +++ b/module/Rest/test/Action/ShortUrl/CreateShortUrlActionTest.php @@ -5,15 +5,13 @@ declare(strict_types=1); namespace ShlinkioTest\Shlink\Rest\Action\ShortUrl; use Cake\Chronos\Chronos; +use CuyZ\Valinor\MapperBuilder; use Laminas\Diactoros\Response\JsonResponse; -use Laminas\Diactoros\ServerRequest; use Laminas\Diactoros\ServerRequestFactory; -use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Shlinkio\Shlink\Core\Config\Options\UrlShortenerOptions; -use Shlinkio\Shlink\Core\Exception\ValidationException; use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl; use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlCreation; use Shlinkio\Shlink\Core\ShortUrl\Model\UrlShorteningResult; @@ -33,27 +31,42 @@ class CreateShortUrlActionTest extends TestCase $this->urlShortener = $this->createMock(UrlShortener::class); $this->transformer = $this->createMock(ShortUrlDataTransformerInterface::class); - $this->action = new CreateShortUrlAction($this->urlShortener, $this->transformer, new UrlShortenerOptions()); + $this->action = new CreateShortUrlAction( + $this->urlShortener, + $this->transformer, + new UrlShortenerOptions(), + new MapperBuilder()->mapper(), + ); } #[Test] public function properShortcodeConversionReturnsData(): void { + $now = Chronos::now()->microsecond(0); $apiKey = ApiKey::create(); $shortUrl = ShortUrl::createFake(); - $expectedMeta = $body = [ - 'longUrl' => 'http://www.domain.com/foo/bar', - 'validSince' => Chronos::now()->toAtomString(), - 'validUntil' => Chronos::now()->toAtomString(), - 'customSlug' => 'foo-bar-baz', - 'maxVisits' => 50, - 'findIfExists' => true, - 'domain' => 'my-domain.com', + $expectedCreation = new ShortUrlCreation( + longUrl: 'http://www.domain.com/foo/bar', + validSince: $now, + validUntil: $now, + customSlug: 'foo-bar-baz', + maxVisits: 50, + findIfExists: true, + domain: 'my-domain.com', + apiKey: $apiKey, + ); + $body = [ + 'longUrl' => $expectedCreation->longUrl, + 'validSince' => $now->toAtomString(), + 'validUntil' => $now->toAtomString(), + 'customSlug' => $expectedCreation->customSlug, + 'maxVisits' => $expectedCreation->maxVisits, + 'findIfExists' => $expectedCreation->findIfExists, + 'domain' => $expectedCreation->domain, ]; - $expectedMeta['apiKey'] = $apiKey; $this->urlShortener->expects($this->once())->method('shorten')->with( - ShortUrlCreation::fromRawData($expectedMeta), + $expectedCreation, )->willReturn(UrlShorteningResult::withoutErrorOnEventDispatching($shortUrl)); $this->transformer->expects($this->once())->method('transform')->with($shortUrl)->willReturn( ['shortUrl' => 'stringified_short_url'], @@ -68,27 +81,4 @@ class CreateShortUrlActionTest extends TestCase self::assertEquals(200, $response->getStatusCode()); self::assertEquals('stringified_short_url', $payload['shortUrl']); } - - #[Test, DataProvider('provideInvalidDomains')] - public function anInvalidDomainReturnsError(string $domain): void - { - $this->urlShortener->expects($this->never())->method('shorten'); - $this->transformer->expects($this->never())->method('transform'); - - $request = (new ServerRequest())->withParsedBody([ - 'longUrl' => 'http://www.domain.com/foo/bar', - 'domain' => $domain, - ])->withAttribute(ApiKey::class, ApiKey::create()); - - $this->expectException(ValidationException::class); - - $this->action->handle($request); - } - - public static function provideInvalidDomains(): iterable - { - yield ['localhost:80000']; - yield ['127.0.0.1']; - yield ['???/&%$&']; - } } diff --git a/module/Rest/test/Action/ShortUrl/SingleStepCreateShortUrlActionTest.php b/module/Rest/test/Action/ShortUrl/SingleStepCreateShortUrlActionTest.php index 7c9510ad..de9e65c5 100644 --- a/module/Rest/test/Action/ShortUrl/SingleStepCreateShortUrlActionTest.php +++ b/module/Rest/test/Action/ShortUrl/SingleStepCreateShortUrlActionTest.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace ShlinkioTest\Shlink\Rest\Action\ShortUrl; +use CuyZ\Valinor\MapperBuilder; use Laminas\Diactoros\ServerRequest; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\MockObject\MockObject; @@ -32,6 +33,7 @@ class SingleStepCreateShortUrlActionTest extends TestCase $this->urlShortener, $transformer, new UrlShortenerOptions(), + new MapperBuilder()->mapper(), ); } @@ -40,11 +42,11 @@ class SingleStepCreateShortUrlActionTest extends TestCase { $apiKey = ApiKey::create(); - $request = (new ServerRequest())->withQueryParams([ + $request = new ServerRequest()->withQueryParams([ 'longUrl' => 'http://foobar.com', ])->withAttribute(ApiKey::class, $apiKey); $this->urlShortener->expects($this->once())->method('shorten')->with( - ShortUrlCreation::fromRawData(['apiKey' => $apiKey, 'longUrl' => 'http://foobar.com']), + new ShortUrlCreation('http://foobar.com', apiKey: $apiKey), )->willReturn(UrlShorteningResult::withoutErrorOnEventDispatching(ShortUrl::createFake())); $resp = $this->action->handle($request);