mirror of
https://github.com/shlinkio/shlink.git
synced 2026-08-03 09:53:00 -05:00
Use valinor to validate and map short URL creation
This commit is contained in:
+1
-1
@@ -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",
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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<mixed> */
|
||||
/**
|
||||
* @extends InputFilter<mixed>
|
||||
* @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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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++) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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)),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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()));
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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',
|
||||
];
|
||||
|
||||
@@ -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 ? [
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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'];
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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'];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'],
|
||||
)),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
*/
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 ['???/&%$&'];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user