Merge pull request #993 from acelaya-forks/feature/short-url-meta-refactoring

Feature/short url meta refactoring
This commit is contained in:
Alejandro Celaya 2021-01-30 23:26:49 +01:00 committed by GitHub
commit 08f4a424e6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
48 changed files with 390 additions and 402 deletions

View file

@ -47,7 +47,7 @@
"predis/predis": "^1.1",
"pugx/shortid-php": "^0.7",
"ramsey/uuid": "^3.9",
"shlinkio/shlink-common": "dev-main#cab9f39 as 3.5",
"shlinkio/shlink-common": "dev-main#b889f5d as 3.5",
"shlinkio/shlink-config": "^1.0",
"shlinkio/shlink-event-dispatcher": "^2.0",
"shlinkio/shlink-importer": "^2.1",

View file

@ -145,7 +145,8 @@ class GenerateShortUrlCommand extends BaseCommand
$doValidateUrl = $this->doValidateUrl($input);
try {
$shortUrl = $this->urlShortener->shorten($longUrl, $tags, ShortUrlMeta::fromRawData([
$shortUrl = $this->urlShortener->shorten(ShortUrlMeta::fromRawData([
ShortUrlMetaInputFilter::LONG_URL => $longUrl,
ShortUrlMetaInputFilter::VALID_SINCE => $this->getOptionWithDeprecatedFallback($input, 'valid-since'),
ShortUrlMetaInputFilter::VALID_UNTIL => $this->getOptionWithDeprecatedFallback($input, 'valid-until'),
ShortUrlMetaInputFilter::CUSTOM_SLUG => $customSlug,
@ -157,6 +158,7 @@ class GenerateShortUrlCommand extends BaseCommand
ShortUrlMetaInputFilter::DOMAIN => $input->getOption('domain'),
ShortUrlMetaInputFilter::SHORT_CODE_LENGTH => $shortCodeLength,
ShortUrlMetaInputFilter::VALIDATE_URL => $doValidateUrl,
ShortUrlMetaInputFilter::TAGS => $tags,
]));
$io->writeln([

View file

@ -43,7 +43,7 @@ class GenerateShortUrlCommandTest extends TestCase
/** @test */
public function properShortCodeIsCreatedIfLongUrlIsCorrect(): void
{
$shortUrl = new ShortUrl('');
$shortUrl = ShortUrl::createEmpty();
$urlToShortCode = $this->urlShortener->shorten(Argument::cetera())->willReturn($shortUrl);
$this->commandTester->execute([
@ -89,14 +89,13 @@ class GenerateShortUrlCommandTest extends TestCase
/** @test */
public function properlyProcessesProvidedTags(): void
{
$shortUrl = new ShortUrl('');
$shortUrl = ShortUrl::createEmpty();
$urlToShortCode = $this->urlShortener->shorten(
Argument::type('string'),
Argument::that(function (array $tags) {
Argument::that(function (ShortUrlMeta $meta) {
$tags = $meta->getTags();
Assert::assertEquals(['foo', 'bar', 'baz', 'boo', 'zar'], $tags);
return $tags;
return true;
}),
Argument::cetera(),
)->willReturn($shortUrl);
$this->commandTester->execute([
@ -116,10 +115,8 @@ class GenerateShortUrlCommandTest extends TestCase
*/
public function urlValidationHasExpectedValueBasedOnProvidedTags(array $options, ?bool $expectedValidateUrl): void
{
$shortUrl = new ShortUrl('');
$shortUrl = ShortUrl::createEmpty();
$urlToShortCode = $this->urlShortener->shorten(
Argument::type('string'),
Argument::type('array'),
Argument::that(function (ShortUrlMeta $meta) use ($expectedValidateUrl) {
Assert::assertEquals($expectedValidateUrl, $meta->doValidateUrl());
return $meta;

View file

@ -103,7 +103,7 @@ class GetVisitsCommandTest extends TestCase
$shortCode = 'abc123';
$this->visitsTracker->info(new ShortUrlIdentifier($shortCode), Argument::any())->willReturn(
new Paginator(new ArrayAdapter([
(new Visit(new ShortUrl(''), new Visitor('bar', 'foo', '')))->locate(
(new Visit(ShortUrl::createEmpty(), new Visitor('bar', 'foo', '')))->locate(
new VisitLocation(new Location('', 'Spain', '', '', 0, 0, '')),
),
])),

View file

@ -42,7 +42,7 @@ class ListShortUrlsCommandTest extends TestCase
// The paginator will return more than one page
$data = [];
for ($i = 0; $i < 50; $i++) {
$data[] = new ShortUrl('url_' . $i);
$data[] = ShortUrl::withLongUrl('url_' . $i);
}
$this->shortUrlService->listShortUrls(Argument::cetera())
@ -64,7 +64,7 @@ class ListShortUrlsCommandTest extends TestCase
// The paginator will return more than one page
$data = [];
for ($i = 0; $i < 30; $i++) {
$data[] = new ShortUrl('url_' . $i);
$data[] = ShortUrl::withLongUrl('url_' . $i);
}
$this->shortUrlService->listShortUrls(ShortUrlsParams::emptyInstance())

View file

@ -41,7 +41,7 @@ class ResolveUrlCommandTest extends TestCase
{
$shortCode = 'abc123';
$expectedUrl = 'http://domain.com/foo/bar';
$shortUrl = new ShortUrl($expectedUrl);
$shortUrl = ShortUrl::withLongUrl($expectedUrl);
$this->urlResolver->resolveShortUrl(new ShortUrlIdentifier($shortCode))->willReturn($shortUrl)
->shouldBeCalledOnce();

View file

@ -77,7 +77,7 @@ class LocateVisitsCommandTest extends TestCase
bool $expectWarningPrint,
array $args
): void {
$visit = new Visit(new ShortUrl(''), new Visitor('', '', '1.2.3.4'));
$visit = new Visit(ShortUrl::createEmpty(), new Visitor('', '', '1.2.3.4'));
$location = new VisitLocation(Location::emptyInstance());
$mockMethodBehavior = $this->invokeHelperMethods($visit, $location);
@ -121,7 +121,7 @@ class LocateVisitsCommandTest extends TestCase
*/
public function localhostAndEmptyAddressesAreIgnored(?string $address, string $message): void
{
$visit = new Visit(new ShortUrl(''), new Visitor('', '', $address));
$visit = new Visit(ShortUrl::createEmpty(), new Visitor('', '', $address));
$location = new VisitLocation(Location::emptyInstance());
$locateVisits = $this->visitService->locateUnlocatedVisits(Argument::cetera())->will(
@ -154,7 +154,7 @@ class LocateVisitsCommandTest extends TestCase
/** @test */
public function errorWhileLocatingIpIsDisplayed(): void
{
$visit = new Visit(new ShortUrl(''), new Visitor('', '', '1.2.3.4'));
$visit = new Visit(ShortUrl::createEmpty(), new Visitor('', '', '1.2.3.4'));
$location = new VisitLocation(Location::emptyInstance());
$locateVisits = $this->visitService->locateUnlocatedVisits(Argument::cetera())->will(

View file

@ -40,26 +40,41 @@ class ShortUrl extends AbstractEntity
private ?string $importOriginalShortCode = null;
private ?ApiKey $authorApiKey = null;
public function __construct(
string $longUrl,
?ShortUrlMeta $meta = null,
private function __construct()
{
}
public static function createEmpty(): self
{
return self::fromMeta(ShortUrlMeta::createEmpty());
}
public static function withLongUrl(string $longUrl): self
{
return self::fromMeta(ShortUrlMeta::fromRawData([ShortUrlMetaInputFilter::LONG_URL => $longUrl]));
}
public static function fromMeta(
ShortUrlMeta $meta,
?ShortUrlRelationResolverInterface $relationResolver = null
) {
$meta = $meta ?? ShortUrlMeta::createEmpty();
): self {
$instance = new self();
$relationResolver = $relationResolver ?? new SimpleShortUrlRelationResolver();
$this->longUrl = $longUrl;
$this->dateCreated = Chronos::now();
$this->visits = new ArrayCollection();
$this->tags = new ArrayCollection();
$this->validSince = $meta->getValidSince();
$this->validUntil = $meta->getValidUntil();
$this->maxVisits = $meta->getMaxVisits();
$this->customSlugWasProvided = $meta->hasCustomSlug();
$this->shortCodeLength = $meta->getShortCodeLength();
$this->shortCode = $meta->getCustomSlug() ?? generateRandomShortCode($this->shortCodeLength);
$this->domain = $relationResolver->resolveDomain($meta->getDomain());
$this->authorApiKey = $meta->getApiKey();
$instance->longUrl = $meta->getLongUrl();
$instance->dateCreated = Chronos::now();
$instance->visits = new ArrayCollection();
$instance->tags = new ArrayCollection();
$instance->validSince = $meta->getValidSince();
$instance->validUntil = $meta->getValidUntil();
$instance->maxVisits = $meta->getMaxVisits();
$instance->customSlugWasProvided = $meta->hasCustomSlug();
$instance->shortCodeLength = $meta->getShortCodeLength();
$instance->shortCode = $meta->getCustomSlug() ?? generateRandomShortCode($instance->shortCodeLength);
$instance->domain = $relationResolver->resolveDomain($meta->getDomain());
$instance->authorApiKey = $meta->getApiKey();
return $instance;
}
public static function fromImport(
@ -68,6 +83,7 @@ class ShortUrl extends AbstractEntity
?ShortUrlRelationResolverInterface $relationResolver = null
): self {
$meta = [
ShortUrlMetaInputFilter::LONG_URL => $url->longUrl(),
ShortUrlMetaInputFilter::DOMAIN => $url->domain(),
ShortUrlMetaInputFilter::VALIDATE_URL => false,
];
@ -75,7 +91,7 @@ class ShortUrl extends AbstractEntity
$meta[ShortUrlMetaInputFilter::CUSTOM_SLUG] = $url->shortCode();
}
$instance = new self($url->longUrl(), ShortUrlMeta::fromRawData($meta), $relationResolver);
$instance = self::fromMeta(ShortUrlMeta::fromRawData($meta), $relationResolver);
$instance->importSource = $url->source();
$instance->importOriginalShortCode = $url->shortCode();
$instance->dateCreated = Chronos::instance($url->createdAt());
@ -207,9 +223,10 @@ class ShortUrl extends AbstractEntity
public function toString(array $domainConfig): string
{
return (string) (new Uri())->withPath($this->shortCode)
->withScheme($domainConfig['schema'] ?? 'http')
->withHost($this->resolveDomain($domainConfig['hostname'] ?? ''));
return (new Uri())->withPath($this->shortCode)
->withScheme($domainConfig['schema'] ?? 'http')
->withHost($this->resolveDomain($domainConfig['hostname'] ?? ''))
->__toString();
}
private function resolveDomain(string $fallback = ''): string

View file

@ -1,37 +0,0 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Core\Model;
final class CreateShortUrlData
{
private string $longUrl;
private array $tags;
private ShortUrlMeta $meta;
public function __construct(string $longUrl, array $tags = [], ?ShortUrlMeta $meta = null)
{
$this->longUrl = $longUrl;
$this->tags = $tags;
$this->meta = $meta ?? ShortUrlMeta::createEmpty();
}
public function getLongUrl(): string
{
return $this->longUrl;
}
/**
* @return string[]
*/
public function getTags(): array
{
return $this->tags;
}
public function getMeta(): ShortUrlMeta
{
return $this->meta;
}
}

View file

@ -25,7 +25,6 @@ final class ShortUrlEdit
private ?int $maxVisits = null;
private ?bool $validateUrl = null;
// Enforce named constructors
private function __construct()
{
}

View file

@ -17,6 +17,7 @@ use const Shlinkio\Shlink\Core\DEFAULT_SHORT_CODES_LENGTH;
final class ShortUrlMeta
{
private string $longUrl;
private ?Chronos $validSince = null;
private ?Chronos $validUntil = null;
private ?string $customSlug = null;
@ -26,15 +27,18 @@ final class ShortUrlMeta
private int $shortCodeLength = 5;
private ?bool $validateUrl = null;
private ?ApiKey $apiKey = null;
private array $tags = [];
// Enforce named constructors
private function __construct()
{
}
public static function createEmpty(): self
{
return new self();
$instance = new self();
$instance->longUrl = '';
return $instance;
}
/**
@ -44,6 +48,7 @@ final class ShortUrlMeta
{
$instance = new self();
$instance->validateAndInit($data);
return $instance;
}
@ -52,11 +57,12 @@ final class ShortUrlMeta
*/
private function validateAndInit(array $data): void
{
$inputFilter = new ShortUrlMetaInputFilter($data);
$inputFilter = new ShortUrlMetaInputFilter($data, true);
if (! $inputFilter->isValid()) {
throw ValidationException::fromInputFilter($inputFilter);
}
$this->longUrl = $inputFilter->getValue(ShortUrlMetaInputFilter::LONG_URL);
$this->validSince = parseDateField($inputFilter->getValue(ShortUrlMetaInputFilter::VALID_SINCE));
$this->validUntil = parseDateField($inputFilter->getValue(ShortUrlMetaInputFilter::VALID_UNTIL));
$this->customSlug = $inputFilter->getValue(ShortUrlMetaInputFilter::CUSTOM_SLUG);
@ -69,6 +75,12 @@ final class ShortUrlMeta
ShortUrlMetaInputFilter::SHORT_CODE_LENGTH,
) ?? DEFAULT_SHORT_CODES_LENGTH;
$this->apiKey = $inputFilter->getValue(ShortUrlMetaInputFilter::API_KEY);
$this->tags = $inputFilter->getValue(ShortUrlMetaInputFilter::TAGS);
}
public function getLongUrl(): string
{
return $this->longUrl;
}
public function getValidSince(): ?Chronos
@ -140,4 +152,12 @@ final class ShortUrlMeta
{
return $this->apiKey;
}
/**
* @return string[]
*/
public function getTags(): array
{
return $this->tags;
}
}

View file

@ -201,14 +201,14 @@ class ShortUrlRepository extends EntitySpecificationRepository implements ShortU
return $qb;
}
public function findOneMatching(string $url, array $tags, ShortUrlMeta $meta): ?ShortUrl
public function findOneMatching(ShortUrlMeta $meta): ?ShortUrl
{
$qb = $this->getEntityManager()->createQueryBuilder();
$qb->select('s')
->from(ShortUrl::class, 's')
->where($qb->expr()->eq('s.longUrl', ':longUrl'))
->setParameter('longUrl', $url)
->setParameter('longUrl', $meta->getLongUrl())
->setMaxResults(1)
->orderBy('s.id');
@ -239,6 +239,7 @@ class ShortUrlRepository extends EntitySpecificationRepository implements ShortU
$this->applySpecification($qb, $apiKey->spec(), 's');
}
$tags = $meta->getTags();
$tagsAmount = count($tags);
if ($tagsAmount === 0) {
return $qb->getQuery()->getOneOrNullResult();

View file

@ -38,7 +38,7 @@ interface ShortUrlRepositoryInterface extends ObjectRepository, EntitySpecificat
public function shortCodeIsInUse(string $slug, ?string $domain, ?Specification $spec = null): bool;
public function findOneMatching(string $url, array $tags, ShortUrlMeta $meta): ?ShortUrl;
public function findOneMatching(ShortUrlMeta $meta): ?ShortUrl;
public function importedUrlExists(ImportedShlinkUrl $url): bool;
}

View file

@ -38,24 +38,23 @@ class UrlShortener implements UrlShortenerInterface
}
/**
* @param string[] $tags
* @throws NonUniqueSlugException
* @throws InvalidUrlException
* @throws Throwable
*/
public function shorten(string $url, array $tags, ShortUrlMeta $meta): ShortUrl
public function shorten(ShortUrlMeta $meta): ShortUrl
{
// First, check if a short URL exists for all provided params
$existingShortUrl = $this->findExistingShortUrlIfExists($url, $tags, $meta);
$existingShortUrl = $this->findExistingShortUrlIfExists($meta);
if ($existingShortUrl !== null) {
return $existingShortUrl;
}
$this->urlValidator->validateUrl($url, $meta->doValidateUrl());
$this->urlValidator->validateUrl($meta->getLongUrl(), $meta->doValidateUrl());
return $this->em->transactional(function () use ($url, $tags, $meta) {
$shortUrl = new ShortUrl($url, $meta, $this->relationResolver);
$shortUrl->setTags($this->tagNamesToEntities($this->em, $tags));
return $this->em->transactional(function () use ($meta) {
$shortUrl = ShortUrl::fromMeta($meta, $this->relationResolver);
$shortUrl->setTags($this->tagNamesToEntities($this->em, $meta->getTags()));
$this->verifyShortCodeUniqueness($meta, $shortUrl);
$this->em->persist($shortUrl);
@ -64,7 +63,7 @@ class UrlShortener implements UrlShortenerInterface
});
}
private function findExistingShortUrlIfExists(string $url, array $tags, ShortUrlMeta $meta): ?ShortUrl
private function findExistingShortUrlIfExists(ShortUrlMeta $meta): ?ShortUrl
{
if (! $meta->findIfExists()) {
return null;
@ -72,7 +71,7 @@ class UrlShortener implements UrlShortenerInterface
/** @var ShortUrlRepositoryInterface $repo */
$repo = $this->em->getRepository(ShortUrl::class);
return $repo->findOneMatching($url, $tags, $meta);
return $repo->findOneMatching($meta);
}
private function verifyShortCodeUniqueness(ShortUrlMeta $meta, ShortUrl $shortUrlToBeCreated): void

View file

@ -12,9 +12,8 @@ use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
interface UrlShortenerInterface
{
/**
* @param string[] $tags
* @throws NonUniqueSlugException
* @throws InvalidUrlException
*/
public function shorten(string $url, array $tags, ShortUrlMeta $meta): ShortUrl;
public function shorten(ShortUrlMeta $meta): ShortUrl;
}

View file

@ -30,16 +30,28 @@ class ShortUrlMetaInputFilter extends InputFilter
public const LONG_URL = 'longUrl';
public const VALIDATE_URL = 'validateUrl';
public const API_KEY = 'apiKey';
public const TAGS = 'tags';
public function __construct(array $data)
private bool $requireLongUrl;
public function __construct(array $data, bool $requireLongUrl = false)
{
$this->requireLongUrl = $requireLongUrl;
$this->initialize();
$this->setData($data);
}
private function initialize(): void
{
$this->add($this->createInput(self::LONG_URL, false));
$longUrlInput = $this->createInput(self::LONG_URL, $this->requireLongUrl);
$longUrlInput->getValidatorChain()->attach(new Validator\NotEmpty([
Validator\NotEmpty::OBJECT,
Validator\NotEmpty::SPACE,
Validator\NotEmpty::NULL,
Validator\NotEmpty::EMPTY_ARRAY,
Validator\NotEmpty::BOOLEAN,
]));
$this->add($longUrlInput);
$validSince = $this->createInput(self::VALID_SINCE, false);
$validSince->getValidatorChain()->attach(new Validator\Date(['format' => DateTime::ATOM]));
@ -63,8 +75,8 @@ class ShortUrlMetaInputFilter extends InputFilter
]));
$this->add($customSlug);
$this->add($this->createPositiveNumberInput(self::MAX_VISITS));
$this->add($this->createPositiveNumberInput(self::SHORT_CODE_LENGTH, MIN_SHORT_CODES_LENGTH));
$this->add($this->createNumericInput(self::MAX_VISITS, false));
$this->add($this->createNumericInput(self::SHORT_CODE_LENGTH, false, MIN_SHORT_CODES_LENGTH));
$this->add($this->createBooleanInput(self::FIND_IF_EXISTS, false));
@ -79,14 +91,7 @@ class ShortUrlMetaInputFilter extends InputFilter
->setRequired(false)
->getValidatorChain()->attach(new Validator\IsInstanceOf(['className' => ApiKey::class]));
$this->add($apiKeyInput);
}
private function createPositiveNumberInput(string $name, int $min = 1): Input
{
$input = $this->createInput($name, false);
$input->getValidatorChain()->attach(new Validator\Digits())
->attach(new Validator\GreaterThan(['min' => $min, 'inclusive' => true]));
return $input;
$this->add($this->createTagsInput(self::TAGS, false));
}
}

View file

@ -4,14 +4,9 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Core\Validation;
use Laminas\Filter;
use Laminas\InputFilter\Input;
use Laminas\InputFilter\InputFilter;
use Laminas\Validator;
use Shlinkio\Shlink\Common\Validation;
use function is_numeric;
class ShortUrlsParamsInputFilter extends InputFilter
{
use Validation\InputFactoryTrait;
@ -36,22 +31,9 @@ class ShortUrlsParamsInputFilter extends InputFilter
$this->add($this->createInput(self::SEARCH_TERM, false));
$this->add($this->createNumericInput(self::PAGE, 1));
$this->add($this->createNumericInput(self::PAGE, false));
$this->add($this->createNumericInput(self::ITEMS_PER_PAGE, false, -1));
$tags = $this->createArrayInput(self::TAGS, false);
$tags->getFilterChain()->attach(new Filter\StringToLower())
->attach(new Filter\PregReplace(['pattern' => '/ /', 'replacement' => '-']));
$this->add($tags);
$this->add($this->createNumericInput(self::ITEMS_PER_PAGE, -1));
}
private function createNumericInput(string $name, int $min): Input
{
$input = $this->createInput($name, false);
$input->getValidatorChain()->attach(new Validator\Callback(fn ($value) => is_numeric($value)))
->attach(new Validator\GreaterThan(['min' => $min, 'inclusive' => true]));
return $input;
$this->add($this->createTagsInput(self::TAGS, false));
}
}

View file

@ -88,9 +88,8 @@ class DomainRepositoryTest extends DatabaseTestCase
private function createShortUrl(Domain $domain, ?ApiKey $apiKey = null): ShortUrl
{
return new ShortUrl(
'foo',
ShortUrlMeta::fromRawData(['domain' => $domain->getAuthority(), 'apiKey' => $apiKey]),
return ShortUrl::fromMeta(
ShortUrlMeta::fromRawData(['domain' => $domain->getAuthority(), 'apiKey' => $apiKey, 'longUrl' => 'foo']),
new class ($domain) implements ShortUrlRelationResolverInterface {
private Domain $domain;

View file

@ -39,16 +39,16 @@ class ShortUrlRepositoryTest extends DatabaseTestCase
/** @test */
public function findOneWithDomainFallbackReturnsProperData(): void
{
$regularOne = new ShortUrl('foo', ShortUrlMeta::fromRawData(['customSlug' => 'foo']));
$regularOne = ShortUrl::fromMeta(ShortUrlMeta::fromRawData(['customSlug' => 'foo', 'longUrl' => 'foo']));
$this->getEntityManager()->persist($regularOne);
$withDomain = new ShortUrl('foo', ShortUrlMeta::fromRawData(
['domain' => 'example.com', 'customSlug' => 'domain-short-code'],
$withDomain = ShortUrl::fromMeta(ShortUrlMeta::fromRawData(
['domain' => 'example.com', 'customSlug' => 'domain-short-code', 'longUrl' => 'foo'],
));
$this->getEntityManager()->persist($withDomain);
$withDomainDuplicatingRegular = new ShortUrl('foo_with_domain', ShortUrlMeta::fromRawData(
['domain' => 'doma.in', 'customSlug' => 'foo'],
$withDomainDuplicatingRegular = ShortUrl::fromMeta(ShortUrlMeta::fromRawData(
['domain' => 'doma.in', 'customSlug' => 'foo', 'longUrl' => 'foo_with_domain'],
));
$this->getEntityManager()->persist($withDomainDuplicatingRegular);
@ -80,7 +80,7 @@ class ShortUrlRepositoryTest extends DatabaseTestCase
{
$count = 5;
for ($i = 0; $i < $count; $i++) {
$this->getEntityManager()->persist(new ShortUrl((string) $i));
$this->getEntityManager()->persist(ShortUrl::withLongUrl((string) $i));
}
$this->getEntityManager()->flush();
@ -93,17 +93,17 @@ class ShortUrlRepositoryTest extends DatabaseTestCase
$tag = new Tag('bar');
$this->getEntityManager()->persist($tag);
$foo = new ShortUrl('foo');
$foo = ShortUrl::withLongUrl('foo');
$foo->setTags(new ArrayCollection([$tag]));
$this->getEntityManager()->persist($foo);
$bar = new ShortUrl('bar');
$bar = ShortUrl::withLongUrl('bar');
$visit = new Visit($bar, Visitor::emptyInstance());
$this->getEntityManager()->persist($visit);
$bar->setVisits(new ArrayCollection([$visit]));
$this->getEntityManager()->persist($bar);
$foo2 = new ShortUrl('foo_2');
$foo2 = ShortUrl::withLongUrl('foo_2');
$ref = new ReflectionObject($foo2);
$dateProp = $ref->getProperty('dateCreated');
$dateProp->setAccessible(true);
@ -151,7 +151,7 @@ class ShortUrlRepositoryTest extends DatabaseTestCase
{
$urls = ['a', 'z', 'c', 'b'];
foreach ($urls as $url) {
$this->getEntityManager()->persist(new ShortUrl($url));
$this->getEntityManager()->persist(ShortUrl::withLongUrl($url));
}
$this->getEntityManager()->flush();
@ -170,12 +170,13 @@ class ShortUrlRepositoryTest extends DatabaseTestCase
/** @test */
public function shortCodeIsInUseLooksForShortUrlInProperSetOfTables(): void
{
$shortUrlWithoutDomain = new ShortUrl('foo', ShortUrlMeta::fromRawData(['customSlug' => 'my-cool-slug']));
$shortUrlWithoutDomain = ShortUrl::fromMeta(
ShortUrlMeta::fromRawData(['customSlug' => 'my-cool-slug', 'longUrl' => 'foo']),
);
$this->getEntityManager()->persist($shortUrlWithoutDomain);
$shortUrlWithDomain = new ShortUrl(
'foo',
ShortUrlMeta::fromRawData(['domain' => 'doma.in', 'customSlug' => 'another-slug']),
$shortUrlWithDomain = ShortUrl::fromMeta(
ShortUrlMeta::fromRawData(['domain' => 'doma.in', 'customSlug' => 'another-slug', 'longUrl' => 'foo']),
);
$this->getEntityManager()->persist($shortUrlWithDomain);
@ -192,12 +193,13 @@ class ShortUrlRepositoryTest extends DatabaseTestCase
/** @test */
public function findOneLooksForShortUrlInProperSetOfTables(): void
{
$shortUrlWithoutDomain = new ShortUrl('foo', ShortUrlMeta::fromRawData(['customSlug' => 'my-cool-slug']));
$shortUrlWithoutDomain = ShortUrl::fromMeta(
ShortUrlMeta::fromRawData(['customSlug' => 'my-cool-slug', 'longUrl' => 'foo']),
);
$this->getEntityManager()->persist($shortUrlWithoutDomain);
$shortUrlWithDomain = new ShortUrl(
'foo',
ShortUrlMeta::fromRawData(['domain' => 'doma.in', 'customSlug' => 'another-slug']),
$shortUrlWithDomain = ShortUrl::fromMeta(
ShortUrlMeta::fromRawData(['domain' => 'doma.in', 'customSlug' => 'another-slug', 'longUrl' => 'foo']),
);
$this->getEntityManager()->persist($shortUrlWithDomain);
@ -214,12 +216,16 @@ class ShortUrlRepositoryTest extends DatabaseTestCase
/** @test */
public function findOneMatchingReturnsNullForNonExistingShortUrls(): void
{
self::assertNull($this->repo->findOneMatching('', [], ShortUrlMeta::createEmpty()));
self::assertNull($this->repo->findOneMatching('foobar', [], ShortUrlMeta::createEmpty()));
self::assertNull($this->repo->findOneMatching('foobar', ['foo', 'bar'], ShortUrlMeta::createEmpty()));
self::assertNull($this->repo->findOneMatching('foobar', ['foo', 'bar'], ShortUrlMeta::fromRawData([
self::assertNull($this->repo->findOneMatching(ShortUrlMeta::createEmpty()));
self::assertNull($this->repo->findOneMatching(ShortUrlMeta::fromRawData(['longUrl' => 'foobar'])));
self::assertNull($this->repo->findOneMatching(
ShortUrlMeta::fromRawData(['longUrl' => 'foobar', 'tags' => ['foo', 'bar']]),
));
self::assertNull($this->repo->findOneMatching(ShortUrlMeta::fromRawData([
'validSince' => Chronos::parse('2020-03-05 20:18:30'),
'customSlug' => 'this_slug_does_not_exist',
'longUrl' => 'foobar',
'tags' => ['foo', 'bar'],
])));
}
@ -229,56 +235,64 @@ class ShortUrlRepositoryTest extends DatabaseTestCase
$start = Chronos::parse('2020-03-05 20:18:30');
$end = Chronos::parse('2021-03-05 20:18:30');
$shortUrl = new ShortUrl('foo', ShortUrlMeta::fromRawData(['validSince' => $start]));
$shortUrl = ShortUrl::fromMeta(ShortUrlMeta::fromRawData(['validSince' => $start, 'longUrl' => 'foo']));
$shortUrl->setTags($this->tagNamesToEntities($this->getEntityManager(), ['foo', 'bar']));
$this->getEntityManager()->persist($shortUrl);
$shortUrl2 = new ShortUrl('bar', ShortUrlMeta::fromRawData(['validUntil' => $end]));
$shortUrl2 = ShortUrl::fromMeta(ShortUrlMeta::fromRawData(['validUntil' => $end, 'longUrl' => 'bar']));
$this->getEntityManager()->persist($shortUrl2);
$shortUrl3 = new ShortUrl('baz', ShortUrlMeta::fromRawData(['validSince' => $start, 'validUntil' => $end]));
$shortUrl3 = ShortUrl::fromMeta(
ShortUrlMeta::fromRawData(['validSince' => $start, 'validUntil' => $end, 'longUrl' => 'baz']),
);
$this->getEntityManager()->persist($shortUrl3);
$shortUrl4 = new ShortUrl('foo', ShortUrlMeta::fromRawData(['customSlug' => 'custom', 'validUntil' => $end]));
$shortUrl4 = ShortUrl::fromMeta(
ShortUrlMeta::fromRawData(['customSlug' => 'custom', 'validUntil' => $end, 'longUrl' => 'foo']),
);
$this->getEntityManager()->persist($shortUrl4);
$shortUrl5 = new ShortUrl('foo', ShortUrlMeta::fromRawData(['maxVisits' => 3]));
$shortUrl5 = ShortUrl::fromMeta(ShortUrlMeta::fromRawData(['maxVisits' => 3, 'longUrl' => 'foo']));
$this->getEntityManager()->persist($shortUrl5);
$shortUrl6 = new ShortUrl('foo', ShortUrlMeta::fromRawData(['domain' => 'doma.in']));
$shortUrl6 = ShortUrl::fromMeta(ShortUrlMeta::fromRawData(['domain' => 'doma.in', 'longUrl' => 'foo']));
$this->getEntityManager()->persist($shortUrl6);
$this->getEntityManager()->flush();
self::assertSame(
$shortUrl,
$this->repo->findOneMatching('foo', ['foo', 'bar'], ShortUrlMeta::fromRawData(['validSince' => $start])),
$this->repo->findOneMatching(
ShortUrlMeta::fromRawData(['validSince' => $start, 'longUrl' => 'foo', 'tags' => ['foo', 'bar']]),
),
);
self::assertSame(
$shortUrl2,
$this->repo->findOneMatching('bar', [], ShortUrlMeta::fromRawData(['validUntil' => $end])),
$this->repo->findOneMatching(ShortUrlMeta::fromRawData(['validUntil' => $end, 'longUrl' => 'bar'])),
);
self::assertSame(
$shortUrl3,
$this->repo->findOneMatching('baz', [], ShortUrlMeta::fromRawData([
$this->repo->findOneMatching(ShortUrlMeta::fromRawData([
'validSince' => $start,
'validUntil' => $end,
'longUrl' => 'baz',
])),
);
self::assertSame(
$shortUrl4,
$this->repo->findOneMatching('foo', [], ShortUrlMeta::fromRawData([
$this->repo->findOneMatching(ShortUrlMeta::fromRawData([
'customSlug' => 'custom',
'validUntil' => $end,
'longUrl' => 'foo',
])),
);
self::assertSame(
$shortUrl5,
$this->repo->findOneMatching('foo', [], ShortUrlMeta::fromRawData(['maxVisits' => 3])),
$this->repo->findOneMatching(ShortUrlMeta::fromRawData(['maxVisits' => 3, 'longUrl' => 'foo'])),
);
self::assertSame(
$shortUrl6,
$this->repo->findOneMatching('foo', [], ShortUrlMeta::fromRawData(['domain' => 'doma.in'])),
$this->repo->findOneMatching(ShortUrlMeta::fromRawData(['domain' => 'doma.in', 'longUrl' => 'foo'])),
);
}
@ -286,25 +300,28 @@ class ShortUrlRepositoryTest extends DatabaseTestCase
public function findOneMatchingReturnsOldestOneWhenThereAreMultipleMatches(): void
{
$start = Chronos::parse('2020-03-05 20:18:30');
$meta = ['validSince' => $start, 'maxVisits' => 50];
$meta = ShortUrlMeta::fromRawData(['validSince' => $start, 'maxVisits' => 50, 'longUrl' => 'foo']);
$tags = ['foo', 'bar'];
$tagEntities = $this->tagNamesToEntities($this->getEntityManager(), $tags);
$metaWithTags = ShortUrlMeta::fromRawData(
['validSince' => $start, 'maxVisits' => 50, 'longUrl' => 'foo', 'tags' => $tags],
);
$shortUrl1 = new ShortUrl('foo', ShortUrlMeta::fromRawData($meta));
$shortUrl1 = ShortUrl::fromMeta($meta);
$shortUrl1->setTags($tagEntities);
$this->getEntityManager()->persist($shortUrl1);
$shortUrl2 = new ShortUrl('foo', ShortUrlMeta::fromRawData($meta));
$shortUrl2 = ShortUrl::fromMeta($meta);
$shortUrl2->setTags($tagEntities);
$this->getEntityManager()->persist($shortUrl2);
$shortUrl3 = new ShortUrl('foo', ShortUrlMeta::fromRawData($meta));
$shortUrl3 = ShortUrl::fromMeta($meta);
$shortUrl3->setTags($tagEntities);
$this->getEntityManager()->persist($shortUrl3);
$this->getEntityManager()->flush();
$result = $this->repo->findOneMatching('foo', $tags, ShortUrlMeta::fromRawData($meta));
$result = $this->repo->findOneMatching($metaWithTags);
self::assertSame($shortUrl1, $result);
self::assertNotSame($shortUrl2, $result);
@ -332,8 +349,8 @@ class ShortUrlRepositoryTest extends DatabaseTestCase
$rightDomainApiKey = ApiKey::withRoles(RoleDefinition::forDomain($rightDomain));
$this->getEntityManager()->persist($rightDomainApiKey);
$shortUrl = new ShortUrl('foo', ShortUrlMeta::fromRawData(
['validSince' => $start, 'apiKey' => $apiKey, 'domain' => $rightDomain->getAuthority()],
$shortUrl = ShortUrl::fromMeta(ShortUrlMeta::fromRawData(
['validSince' => $start, 'apiKey' => $apiKey, 'domain' => $rightDomain->getAuthority(), 'longUrl' => 'foo'],
), new PersistenceShortUrlRelationResolver($this->getEntityManager()));
$shortUrl->setTags($this->tagNamesToEntities($this->getEntityManager(), ['foo', 'bar']));
$this->getEntityManager()->persist($shortUrl);
@ -342,45 +359,59 @@ class ShortUrlRepositoryTest extends DatabaseTestCase
self::assertSame(
$shortUrl,
$this->repo->findOneMatching('foo', ['foo', 'bar'], ShortUrlMeta::fromRawData(['validSince' => $start])),
$this->repo->findOneMatching(
ShortUrlMeta::fromRawData(['validSince' => $start, 'longUrl' => 'foo', 'tags' => ['foo', 'bar']]),
),
);
self::assertSame($shortUrl, $this->repo->findOneMatching('foo', ['foo', 'bar'], ShortUrlMeta::fromRawData([
self::assertSame($shortUrl, $this->repo->findOneMatching(ShortUrlMeta::fromRawData([
'validSince' => $start,
'apiKey' => $apiKey,
'longUrl' => 'foo',
'tags' => ['foo', 'bar'],
])));
self::assertNull($this->repo->findOneMatching('foo', ['foo', 'bar'], ShortUrlMeta::fromRawData([
self::assertNull($this->repo->findOneMatching(ShortUrlMeta::fromRawData([
'validSince' => $start,
'apiKey' => $otherApiKey,
'longUrl' => 'foo',
'tags' => ['foo', 'bar'],
])));
self::assertSame(
$shortUrl,
$this->repo->findOneMatching('foo', ['foo', 'bar'], ShortUrlMeta::fromRawData([
$this->repo->findOneMatching(ShortUrlMeta::fromRawData([
'validSince' => $start,
'domain' => $rightDomain->getAuthority(),
'longUrl' => 'foo',
'tags' => ['foo', 'bar'],
])),
);
self::assertSame(
$shortUrl,
$this->repo->findOneMatching('foo', ['foo', 'bar'], ShortUrlMeta::fromRawData([
$this->repo->findOneMatching(ShortUrlMeta::fromRawData([
'validSince' => $start,
'domain' => $rightDomain->getAuthority(),
'apiKey' => $rightDomainApiKey,
'longUrl' => 'foo',
'tags' => ['foo', 'bar'],
])),
);
self::assertSame(
$shortUrl,
$this->repo->findOneMatching('foo', ['foo', 'bar'], ShortUrlMeta::fromRawData([
$this->repo->findOneMatching(ShortUrlMeta::fromRawData([
'validSince' => $start,
'domain' => $rightDomain->getAuthority(),
'apiKey' => $apiKey,
'longUrl' => 'foo',
'tags' => ['foo', 'bar'],
])),
);
self::assertNull(
$this->repo->findOneMatching('foo', ['foo', 'bar'], ShortUrlMeta::fromRawData([
$this->repo->findOneMatching(ShortUrlMeta::fromRawData([
'validSince' => $start,
'domain' => $rightDomain->getAuthority(),
'apiKey' => $wrongDomainApiKey,
'longUrl' => 'foo',
'tags' => ['foo', 'bar'],
])),
);
}

View file

@ -62,14 +62,14 @@ class TagRepositoryTest extends DatabaseTestCase
[$firstUrlTags] = array_chunk($tags, 3);
$secondUrlTags = [$tags[0]];
$shortUrl = new ShortUrl('');
$shortUrl = ShortUrl::createEmpty();
$shortUrl->setTags(new ArrayCollection($firstUrlTags));
$this->getEntityManager()->persist($shortUrl);
$this->getEntityManager()->persist(new Visit($shortUrl, Visitor::emptyInstance()));
$this->getEntityManager()->persist(new Visit($shortUrl, Visitor::emptyInstance()));
$this->getEntityManager()->persist(new Visit($shortUrl, Visitor::emptyInstance()));
$shortUrl2 = new ShortUrl('');
$shortUrl2 = ShortUrl::createEmpty();
$shortUrl2->setTags(new ArrayCollection($secondUrlTags));
$this->getEntityManager()->persist($shortUrl2);
$this->getEntityManager()->persist(new Visit($shortUrl2, Visitor::emptyInstance()));
@ -119,13 +119,12 @@ class TagRepositoryTest extends DatabaseTestCase
[$firstUrlTags, $secondUrlTags] = array_chunk($tags, 3);
$shortUrl = new ShortUrl('', ShortUrlMeta::fromRawData(['apiKey' => $authorApiKey]));
$shortUrl = ShortUrl::fromMeta(ShortUrlMeta::fromRawData(['apiKey' => $authorApiKey, 'longUrl' => '']));
$shortUrl->setTags(new ArrayCollection($firstUrlTags));
$this->getEntityManager()->persist($shortUrl);
$shortUrl2 = new ShortUrl(
'',
ShortUrlMeta::fromRawData(['domain' => $domain->getAuthority()]),
$shortUrl2 = ShortUrl::fromMeta(
ShortUrlMeta::fromRawData(['domain' => $domain->getAuthority(), 'longUrl' => '']),
new PersistenceShortUrlRelationResolver($this->getEntityManager()),
);
$shortUrl2->setTags(new ArrayCollection($secondUrlTags));

View file

@ -40,7 +40,7 @@ class VisitRepositoryTest extends DatabaseTestCase
*/
public function findVisitsReturnsProperVisits(int $blockSize): void
{
$shortUrl = new ShortUrl('');
$shortUrl = ShortUrl::createEmpty();
$this->getEntityManager()->persist($shortUrl);
$countIterable = function (iterable $results): int {
$resultsCount = 0;
@ -190,9 +190,8 @@ class VisitRepositoryTest extends DatabaseTestCase
$apiKey1 = ApiKey::withRoles(RoleDefinition::forAuthoredShortUrls());
$this->getEntityManager()->persist($apiKey1);
$shortUrl = new ShortUrl(
'',
ShortUrlMeta::fromRawData(['apiKey' => $apiKey1, 'domain' => $domain->getAuthority()]),
$shortUrl = ShortUrl::fromMeta(
ShortUrlMeta::fromRawData(['apiKey' => $apiKey1, 'domain' => $domain->getAuthority(), 'longUrl' => '']),
new PersistenceShortUrlRelationResolver($this->getEntityManager()),
);
$this->getEntityManager()->persist($shortUrl);
@ -200,13 +199,12 @@ class VisitRepositoryTest extends DatabaseTestCase
$apiKey2 = ApiKey::withRoles(RoleDefinition::forAuthoredShortUrls());
$this->getEntityManager()->persist($apiKey2);
$shortUrl2 = new ShortUrl('', ShortUrlMeta::fromRawData(['apiKey' => $apiKey2]));
$shortUrl2 = ShortUrl::fromMeta(ShortUrlMeta::fromRawData(['apiKey' => $apiKey2, 'longUrl' => '']));
$this->getEntityManager()->persist($shortUrl2);
$this->createVisitsForShortUrl($shortUrl2, 5);
$shortUrl3 = new ShortUrl(
'',
ShortUrlMeta::fromRawData(['apiKey' => $apiKey2, 'domain' => $domain->getAuthority()]),
$shortUrl3 = ShortUrl::fromMeta(
ShortUrlMeta::fromRawData(['apiKey' => $apiKey2, 'domain' => $domain->getAuthority(), 'longUrl' => '']),
new PersistenceShortUrlRelationResolver($this->getEntityManager()),
);
$this->getEntityManager()->persist($shortUrl3);
@ -225,7 +223,7 @@ class VisitRepositoryTest extends DatabaseTestCase
private function createShortUrlsAndVisits(bool $withDomain = true): array
{
$shortUrl = new ShortUrl('');
$shortUrl = ShortUrl::createEmpty();
$domain = 'example.com';
$shortCode = $shortUrl->getShortCode();
$this->getEntityManager()->persist($shortUrl);
@ -233,9 +231,10 @@ class VisitRepositoryTest extends DatabaseTestCase
$this->createVisitsForShortUrl($shortUrl);
if ($withDomain) {
$shortUrlWithDomain = new ShortUrl('', ShortUrlMeta::fromRawData([
$shortUrlWithDomain = ShortUrl::fromMeta(ShortUrlMeta::fromRawData([
'customSlug' => $shortCode,
'domain' => $domain,
'longUrl' => '',
]));
$this->getEntityManager()->persist($shortUrlWithDomain);
$this->createVisitsForShortUrl($shortUrlWithDomain, 3);

View file

@ -43,7 +43,7 @@ class PixelActionTest extends TestCase
{
$shortCode = 'abc123';
$this->urlResolver->resolveEnabledShortUrl(new ShortUrlIdentifier($shortCode, ''))->willReturn(
new ShortUrl('http://domain.com/foo/bar'),
ShortUrl::withLongUrl('http://domain.com/foo/bar'),
)->shouldBeCalledOnce();
$this->visitTracker->track(Argument::cetera())->shouldBeCalledOnce();

View file

@ -60,7 +60,7 @@ class QrCodeActionTest extends TestCase
{
$shortCode = 'abc123';
$this->urlResolver->resolveEnabledShortUrl(new ShortUrlIdentifier($shortCode, ''))
->willReturn(new ShortUrl(''))
->willReturn(ShortUrl::createEmpty())
->shouldBeCalledOnce();
$delegate = $this->prophesize(RequestHandlerInterface::class);
@ -83,7 +83,9 @@ class QrCodeActionTest extends TestCase
string $expectedContentType
): void {
$code = 'abc123';
$this->urlResolver->resolveEnabledShortUrl(new ShortUrlIdentifier($code, ''))->willReturn(new ShortUrl(''));
$this->urlResolver->resolveEnabledShortUrl(new ShortUrlIdentifier($code, ''))->willReturn(
ShortUrl::createEmpty(),
);
$delegate = $this->prophesize(RequestHandlerInterface::class);
$req = (new ServerRequest())->withAttribute('shortCode', $code)->withQueryParams($query);
@ -107,7 +109,9 @@ class QrCodeActionTest extends TestCase
public function imageIsReturnedWithExpectedSize(ServerRequestInterface $req, int $expectedSize): void
{
$code = 'abc123';
$this->urlResolver->resolveEnabledShortUrl(new ShortUrlIdentifier($code, ''))->willReturn(new ShortUrl(''));
$this->urlResolver->resolveEnabledShortUrl(new ShortUrlIdentifier($code, ''))->willReturn(
ShortUrl::createEmpty(),
);
$delegate = $this->prophesize(RequestHandlerInterface::class);
$resp = $this->action->process($req->withAttribute('shortCode', $code), $delegate->reveal());

View file

@ -54,7 +54,7 @@ class RedirectActionTest extends TestCase
public function redirectionIsPerformedToLongUrl(string $expectedUrl, array $query): void
{
$shortCode = 'abc123';
$shortUrl = new ShortUrl('http://domain.com/foo/bar?some=thing');
$shortUrl = ShortUrl::withLongUrl('http://domain.com/foo/bar?some=thing');
$shortCodeToUrl = $this->urlResolver->resolveEnabledShortUrl(
new ShortUrlIdentifier($shortCode, ''),
)->willReturn($shortUrl);
@ -104,7 +104,7 @@ class RedirectActionTest extends TestCase
public function trackingIsDisabledWhenRequestIsForwardedFromHead(): void
{
$shortCode = 'abc123';
$shortUrl = new ShortUrl('http://domain.com/foo/bar?some=thing');
$shortUrl = ShortUrl::withLongUrl('http://domain.com/foo/bar?some=thing');
$this->urlResolver->resolveEnabledShortUrl(new ShortUrlIdentifier($shortCode, ''))->willReturn($shortUrl);
$track = $this->visitTracker->track(Argument::cetera())->will(function (): void {
});

View file

@ -37,11 +37,11 @@ class ShortUrlTest extends TestCase
public function provideInvalidShortUrls(): iterable
{
yield 'with custom slug' => [
new ShortUrl('', ShortUrlMeta::fromRawData(['customSlug' => 'custom-slug'])),
ShortUrl::fromMeta(ShortUrlMeta::fromRawData(['customSlug' => 'custom-slug', 'longUrl' => ''])),
'The short code cannot be regenerated on ShortUrls where a custom slug was provided.',
];
yield 'already persisted' => [
(new ShortUrl(''))->setId('1'),
ShortUrl::createEmpty()->setId('1'),
'The short code can be regenerated only on new ShortUrls which have not been persisted yet.',
];
}
@ -62,7 +62,7 @@ class ShortUrlTest extends TestCase
public function provideValidShortUrls(): iterable
{
yield 'no custom slug' => [new ShortUrl('')];
yield 'no custom slug' => [ShortUrl::createEmpty()];
yield 'imported with custom slug' => [
ShortUrl::fromImport(new ImportedShlinkUrl('', '', [], Chronos::now(), null, 'custom-slug'), true),
];
@ -74,8 +74,8 @@ class ShortUrlTest extends TestCase
*/
public function shortCodesHaveExpectedLength(?int $length, int $expectedLength): void
{
$shortUrl = new ShortUrl('', ShortUrlMeta::fromRawData(
[ShortUrlMetaInputFilter::SHORT_CODE_LENGTH => $length],
$shortUrl = ShortUrl::fromMeta(ShortUrlMeta::fromRawData(
[ShortUrlMetaInputFilter::SHORT_CODE_LENGTH => $length, 'longUrl' => ''],
));
self::assertEquals($expectedLength, strlen($shortUrl->getShortCode()));

View file

@ -19,7 +19,7 @@ class VisitTest extends TestCase
*/
public function isProperlyJsonSerialized(?Chronos $date): void
{
$visit = new Visit(new ShortUrl(''), new Visitor('Chrome', 'some site', '1.2.3.4'), true, $date);
$visit = new Visit(ShortUrl::createEmpty(), new Visitor('Chrome', 'some site', '1.2.3.4'), true, $date);
self::assertEquals([
'referer' => 'some site',
@ -41,7 +41,7 @@ class VisitTest extends TestCase
*/
public function addressIsAnonymizedWhenRequested(bool $anonymize, ?string $address, ?string $expectedAddress): void
{
$visit = new Visit(new ShortUrl(''), new Visitor('Chrome', 'some site', $address), $anonymize);
$visit = new Visit(ShortUrl::createEmpty(), new Visitor('Chrome', 'some site', $address), $anonymize);
self::assertEquals($expectedAddress, $visit->getRemoteAddr());
}

View file

@ -78,7 +78,7 @@ class LocateShortUrlVisitTest extends TestCase
{
$event = new ShortUrlVisited('123');
$findVisit = $this->em->find(Visit::class, '123')->willReturn(
new Visit(new ShortUrl(''), new Visitor('', '', '1.2.3.4')),
new Visit(ShortUrl::createEmpty(), new Visitor('', '', '1.2.3.4')),
);
$resolveLocation = $this->ipLocationResolver->resolveIpLocation(Argument::cetera())->willThrow(
WrongIpException::class,
@ -125,7 +125,7 @@ class LocateShortUrlVisitTest extends TestCase
public function provideNonLocatableVisits(): iterable
{
$shortUrl = new ShortUrl('');
$shortUrl = ShortUrl::createEmpty();
yield 'null IP' => [new Visit($shortUrl, new Visitor('', '', null))];
yield 'empty IP' => [new Visit($shortUrl, new Visitor('', '', ''))];
@ -139,7 +139,7 @@ class LocateShortUrlVisitTest extends TestCase
public function locatableVisitsResolveToLocation(string $anonymizedIpAddress, ?string $originalIpAddress): void
{
$ipAddr = $originalIpAddress ?? $anonymizedIpAddress;
$visit = new Visit(new ShortUrl(''), new Visitor('', '', $ipAddr));
$visit = new Visit(ShortUrl::createEmpty(), new Visitor('', '', $ipAddr));
$location = new Location('', '', '', '', 0.0, 0.0, '');
$event = new ShortUrlVisited('123', $originalIpAddress);
@ -171,7 +171,7 @@ class LocateShortUrlVisitTest extends TestCase
{
$e = GeolocationDbUpdateFailedException::create(true);
$ipAddr = '1.2.3.0';
$visit = new Visit(new ShortUrl(''), new Visitor('', '', $ipAddr));
$visit = new Visit(ShortUrl::createEmpty(), new Visitor('', '', $ipAddr));
$location = new Location('', '', '', '', 0.0, 0.0, '');
$event = new ShortUrlVisited('123');
@ -202,7 +202,7 @@ class LocateShortUrlVisitTest extends TestCase
{
$e = GeolocationDbUpdateFailedException::create(false);
$ipAddr = '1.2.3.0';
$visit = new Visit(new ShortUrl(''), new Visitor('', '', $ipAddr));
$visit = new Visit(ShortUrl::createEmpty(), new Visitor('', '', $ipAddr));
$location = new Location('', '', '', '', 0.0, 0.0, '');
$event = new ShortUrlVisited('123');

View file

@ -77,7 +77,7 @@ class NotifyVisitToMercureTest extends TestCase
public function notificationsAreSentWhenVisitIsFound(): void
{
$visitId = '123';
$visit = new Visit(new ShortUrl(''), Visitor::emptyInstance());
$visit = new Visit(ShortUrl::createEmpty(), Visitor::emptyInstance());
$update = new Update('', '');
$findVisit = $this->em->find(Visit::class, $visitId)->willReturn($visit);
@ -101,7 +101,7 @@ class NotifyVisitToMercureTest extends TestCase
public function debugIsLoggedWhenExceptionIsThrown(): void
{
$visitId = '123';
$visit = new Visit(new ShortUrl(''), Visitor::emptyInstance());
$visit = new Visit(ShortUrl::createEmpty(), Visitor::emptyInstance());
$update = new Update('', '');
$e = new RuntimeException('Error');

View file

@ -79,7 +79,9 @@ class NotifyVisitToWebHooksTest extends TestCase
$webhooks = ['foo', 'invalid', 'bar', 'baz'];
$invalidWebhooks = ['invalid', 'baz'];
$find = $this->em->find(Visit::class, '1')->willReturn(new Visit(new ShortUrl(''), Visitor::emptyInstance()));
$find = $this->em->find(Visit::class, '1')->willReturn(
new Visit(ShortUrl::createEmpty(), Visitor::emptyInstance()),
);
$requestAsync = $this->httpClient->requestAsync(
RequestMethodInterface::METHOD_POST,
Argument::type('string'),

View file

@ -28,7 +28,7 @@ class MercureUpdatesGeneratorTest extends TestCase
*/
public function visitIsProperlySerializedIntoUpdate(string $method, string $expectedTopic): void
{
$shortUrl = new ShortUrl('', ShortUrlMeta::fromRawData(['customSlug' => 'foo']));
$shortUrl = ShortUrl::fromMeta(ShortUrlMeta::fromRawData(['customSlug' => 'foo', 'longUrl' => '']));
$visit = new Visit($shortUrl, Visitor::emptyInstance());
$update = $this->generator->{$method}($visit);

View file

@ -56,6 +56,9 @@ class ShortUrlMetaTest extends TestCase
yield [[
ShortUrlMetaInputFilter::CUSTOM_SLUG => ' ',
]];
yield [[
ShortUrlMetaInputFilter::LONG_URL => [],
]];
}
/**
@ -64,9 +67,11 @@ class ShortUrlMetaTest extends TestCase
*/
public function properlyCreatedInstanceReturnsValues(string $customSlug, string $expectedSlug): void
{
$meta = ShortUrlMeta::fromRawData(
['validSince' => Chronos::parse('2015-01-01')->toAtomString(), 'customSlug' => $customSlug],
);
$meta = ShortUrlMeta::fromRawData([
'validSince' => Chronos::parse('2015-01-01')->toAtomString(),
'customSlug' => $customSlug,
'longUrl' => '',
]);
self::assertTrue($meta->hasValidSince());
self::assertEquals(Chronos::parse('2015-01-01'), $meta->getValidSince());

View file

@ -33,8 +33,8 @@ class DeleteShortUrlServiceTest extends TestCase
public function setUp(): void
{
$shortUrl = (new ShortUrl(''))->setVisits(new ArrayCollection(
map(range(0, 10), fn () => new Visit(new ShortUrl(''), Visitor::emptyInstance())),
$shortUrl = ShortUrl::createEmpty()->setVisits(new ArrayCollection(
map(range(0, 10), fn () => new Visit(ShortUrl::createEmpty(), Visitor::emptyInstance())),
));
$this->shortCode = $shortUrl->getShortCode();

View file

@ -44,7 +44,7 @@ class ShortUrlResolverTest extends TestCase
*/
public function shortCodeIsProperlyParsed(?ApiKey $apiKey): void
{
$shortUrl = new ShortUrl('expected_url');
$shortUrl = ShortUrl::withLongUrl('expected_url');
$shortCode = $shortUrl->getShortCode();
$repo = $this->prophesize(ShortUrlRepositoryInterface::class);
@ -80,7 +80,7 @@ class ShortUrlResolverTest extends TestCase
/** @test */
public function shortCodeToEnabledShortUrlProperlyParsesShortCode(): void
{
$shortUrl = new ShortUrl('expected_url');
$shortUrl = ShortUrl::withLongUrl('expected_url');
$shortCode = $shortUrl->getShortCode();
$repo = $this->prophesize(ShortUrlRepositoryInterface::class);
@ -118,7 +118,7 @@ class ShortUrlResolverTest extends TestCase
$now = Chronos::now();
yield 'maxVisits reached' => [(function () {
$shortUrl = new ShortUrl('', ShortUrlMeta::fromRawData(['maxVisits' => 3]));
$shortUrl = ShortUrl::fromMeta(ShortUrlMeta::fromRawData(['maxVisits' => 3, 'longUrl' => '']));
$shortUrl->setVisits(new ArrayCollection(map(
range(0, 4),
fn () => new Visit($shortUrl, Visitor::emptyInstance()),
@ -126,16 +126,17 @@ class ShortUrlResolverTest extends TestCase
return $shortUrl;
})()];
yield 'future validSince' => [new ShortUrl('', ShortUrlMeta::fromRawData([
'validSince' => $now->addMonth()->toAtomString(),
]))];
yield 'past validUntil' => [new ShortUrl('', ShortUrlMeta::fromRawData([
'validUntil' => $now->subMonth()->toAtomString(),
]))];
yield 'future validSince' => [ShortUrl::fromMeta(ShortUrlMeta::fromRawData(
['validSince' => $now->addMonth()->toAtomString(), 'longUrl' => ''],
))];
yield 'past validUntil' => [ShortUrl::fromMeta(ShortUrlMeta::fromRawData(
['validUntil' => $now->subMonth()->toAtomString(), 'longUrl' => ''],
))];
yield 'mixed' => [(function () use ($now) {
$shortUrl = new ShortUrl('', ShortUrlMeta::fromRawData([
$shortUrl = ShortUrl::fromMeta(ShortUrlMeta::fromRawData([
'maxVisits' => 3,
'validUntil' => $now->subMonth()->toAtomString(),
'longUrl' => '',
]));
$shortUrl->setVisits(new ArrayCollection(map(
range(0, 4),

View file

@ -58,10 +58,10 @@ class ShortUrlServiceTest extends TestCase
public function listedUrlsAreReturnedFromEntityManager(?ApiKey $apiKey): void
{
$list = [
new ShortUrl(''),
new ShortUrl(''),
new ShortUrl(''),
new ShortUrl(''),
ShortUrl::createEmpty(),
ShortUrl::createEmpty(),
ShortUrl::createEmpty(),
ShortUrl::createEmpty(),
];
$repo = $this->prophesize(ShortUrlRepository::class);
@ -106,7 +106,7 @@ class ShortUrlServiceTest extends TestCase
?ApiKey $apiKey
): void {
$originalLongUrl = 'originalLongUrl';
$shortUrl = new ShortUrl($originalLongUrl);
$shortUrl = ShortUrl::withLongUrl($originalLongUrl);
$findShortUrl = $this->urlResolver->resolveShortUrl(
new ShortUrlIdentifier('abc123'),

View file

@ -69,9 +69,7 @@ class UrlShortenerTest extends TestCase
public function urlIsProperlyShortened(): void
{
$shortUrl = $this->urlShortener->shorten(
'http://foobar.com/12345/hello?foo=bar',
[],
ShortUrlMeta::createEmpty(),
ShortUrlMeta::fromRawData(['longUrl' => 'http://foobar.com/12345/hello?foo=bar']),
);
self::assertEquals('http://foobar.com/12345/hello?foo=bar', $shortUrl->getLongUrl());
@ -85,28 +83,22 @@ class UrlShortenerTest extends TestCase
$ensureUniqueness->shouldBeCalledOnce();
$this->expectException(NonUniqueSlugException::class);
$this->urlShortener->shorten(
'http://foobar.com/12345/hello?foo=bar',
[],
ShortUrlMeta::fromRawData(['customSlug' => 'custom-slug']),
);
$this->urlShortener->shorten(ShortUrlMeta::fromRawData(
['customSlug' => 'custom-slug', 'longUrl' => 'http://foobar.com/12345/hello?foo=bar'],
));
}
/**
* @test
* @dataProvider provideExistingShortUrls
*/
public function existingShortUrlIsReturnedWhenRequested(
string $url,
array $tags,
ShortUrlMeta $meta,
ShortUrl $expected
): void {
public function existingShortUrlIsReturnedWhenRequested(ShortUrlMeta $meta, ShortUrl $expected): void
{
$repo = $this->prophesize(ShortUrlRepository::class);
$findExisting = $repo->findOneMatching(Argument::cetera())->willReturn($expected);
$getRepo = $this->em->getRepository(ShortUrl::class)->willReturn($repo->reveal());
$result = $this->urlShortener->shorten($url, $tags, $meta);
$result = $this->urlShortener->shorten($meta);
$findExisting->shouldHaveBeenCalledOnce();
$getRepo->shouldHaveBeenCalledOnce();
@ -119,52 +111,53 @@ class UrlShortenerTest extends TestCase
{
$url = 'http://foo.com';
yield [$url, [], ShortUrlMeta::fromRawData(['findIfExists' => true]), new ShortUrl($url)];
yield [$url, [], ShortUrlMeta::fromRawData(
['findIfExists' => true, 'customSlug' => 'foo'],
), new ShortUrl($url)];
yield [
yield [ShortUrlMeta::fromRawData(['findIfExists' => true, 'longUrl' => $url]), ShortUrl::withLongUrl(
$url,
['foo', 'bar'],
ShortUrlMeta::fromRawData(['findIfExists' => true]),
(new ShortUrl($url))->setTags(new ArrayCollection([new Tag('bar'), new Tag('foo')])),
)];
yield [ShortUrlMeta::fromRawData(
['findIfExists' => true, 'customSlug' => 'foo', 'longUrl' => $url],
), ShortUrl::withLongUrl($url)];
yield [
ShortUrlMeta::fromRawData(['findIfExists' => true, 'longUrl' => $url, 'tags' => ['foo', 'bar']]),
ShortUrl::withLongUrl($url)->setTags(new ArrayCollection([new Tag('bar'), new Tag('foo')])),
];
yield [
$url,
[],
ShortUrlMeta::fromRawData(['findIfExists' => true, 'maxVisits' => 3]),
new ShortUrl($url, ShortUrlMeta::fromRawData(['maxVisits' => 3])),
ShortUrlMeta::fromRawData(['findIfExists' => true, 'maxVisits' => 3, 'longUrl' => $url]),
ShortUrl::fromMeta(ShortUrlMeta::fromRawData(['maxVisits' => 3, 'longUrl' => $url])),
];
yield [
$url,
[],
ShortUrlMeta::fromRawData(['findIfExists' => true, 'validSince' => Chronos::parse('2017-01-01')]),
new ShortUrl($url, ShortUrlMeta::fromRawData(['validSince' => Chronos::parse('2017-01-01')])),
ShortUrlMeta::fromRawData(
['findIfExists' => true, 'validSince' => Chronos::parse('2017-01-01'), 'longUrl' => $url],
),
ShortUrl::fromMeta(
ShortUrlMeta::fromRawData(['validSince' => Chronos::parse('2017-01-01'), 'longUrl' => $url]),
),
];
yield [
$url,
[],
ShortUrlMeta::fromRawData(['findIfExists' => true, 'validUntil' => Chronos::parse('2017-01-01')]),
new ShortUrl($url, ShortUrlMeta::fromRawData(['validUntil' => Chronos::parse('2017-01-01')])),
ShortUrlMeta::fromRawData(
['findIfExists' => true, 'validUntil' => Chronos::parse('2017-01-01'), 'longUrl' => $url],
),
ShortUrl::fromMeta(
ShortUrlMeta::fromRawData(['validUntil' => Chronos::parse('2017-01-01'), 'longUrl' => $url]),
),
];
yield [
$url,
[],
ShortUrlMeta::fromRawData(['findIfExists' => true, 'domain' => 'example.com']),
new ShortUrl($url, ShortUrlMeta::fromRawData(['domain' => 'example.com'])),
ShortUrlMeta::fromRawData(['findIfExists' => true, 'domain' => 'example.com', 'longUrl' => $url]),
ShortUrl::fromMeta(ShortUrlMeta::fromRawData(['domain' => 'example.com', 'longUrl' => $url])),
];
yield [
$url,
['baz', 'foo', 'bar'],
ShortUrlMeta::fromRawData([
'findIfExists' => true,
'validUntil' => Chronos::parse('2017-01-01'),
'maxVisits' => 4,
'longUrl' => $url,
'tags' => ['baz', 'foo', 'bar'],
]),
(new ShortUrl($url, ShortUrlMeta::fromRawData([
ShortUrl::fromMeta(ShortUrlMeta::fromRawData([
'validUntil' => Chronos::parse('2017-01-01'),
'maxVisits' => 4,
])))->setTags(new ArrayCollection([new Tag('foo'), new Tag('bar'), new Tag('baz')])),
'longUrl' => $url,
]))->setTags(new ArrayCollection([new Tag('foo'), new Tag('bar'), new Tag('baz')])),
];
}
}

View file

@ -56,7 +56,7 @@ class VisitsTrackerTest extends TestCase
$this->em->persist(Argument::that(fn (Visit $visit) => $visit->setId('1')))->shouldBeCalledOnce();
$this->em->flush()->shouldBeCalledOnce();
$this->visitsTracker->track(new ShortUrl($shortCode), Visitor::emptyInstance());
$this->visitsTracker->track(ShortUrl::withLongUrl($shortCode), Visitor::emptyInstance());
$this->eventDispatcher->dispatch(Argument::type(ShortUrlVisited::class))->shouldHaveBeenCalled();
}
@ -73,7 +73,7 @@ class VisitsTrackerTest extends TestCase
$count = $repo->shortCodeIsInUse($shortCode, null, $spec)->willReturn(true);
$this->em->getRepository(ShortUrl::class)->willReturn($repo->reveal())->shouldBeCalledOnce();
$list = map(range(0, 1), fn () => new Visit(new ShortUrl(''), Visitor::emptyInstance()));
$list = map(range(0, 1), fn () => new Visit(ShortUrl::createEmpty(), Visitor::emptyInstance()));
$repo2 = $this->prophesize(VisitRepository::class);
$repo2->findVisitsByShortCode($shortCode, null, Argument::type(DateRange::class), 1, 0, $spec)->willReturn(
$list,
@ -129,7 +129,7 @@ class VisitsTrackerTest extends TestCase
$getRepo = $this->em->getRepository(Tag::class)->willReturn($repo->reveal());
$spec = $apiKey === null ? null : $apiKey->spec();
$list = map(range(0, 1), fn () => new Visit(new ShortUrl(''), Visitor::emptyInstance()));
$list = map(range(0, 1), fn () => new Visit(ShortUrl::createEmpty(), Visitor::emptyInstance()));
$repo2 = $this->prophesize(VisitRepository::class);
$repo2->findVisitsByTag($tag, Argument::type(DateRange::class), 1, 0, $spec)->willReturn($list);
$repo2->countVisitsByTag($tag, Argument::type(DateRange::class), $spec)->willReturn(1);

View file

@ -37,18 +37,23 @@ class ShortUrlDataTransformerTest extends TestCase
$maxVisits = random_int(1, 1000);
$now = Chronos::now();
yield 'no metadata' => [new ShortUrl('', ShortUrlMeta::createEmpty()), [
yield 'no metadata' => [ShortUrl::createEmpty(), [
'validSince' => null,
'validUntil' => null,
'maxVisits' => null,
]];
yield 'max visits only' => [new ShortUrl('', ShortUrlMeta::fromRawData(['maxVisits' => $maxVisits])), [
yield 'max visits only' => [ShortUrl::fromMeta(ShortUrlMeta::fromRawData([
'maxVisits' => $maxVisits,
'longUrl' => '',
])), [
'validSince' => null,
'validUntil' => null,
'maxVisits' => $maxVisits,
]];
yield 'max visits and valid since' => [
new ShortUrl('', ShortUrlMeta::fromRawData(['validSince' => $now, 'maxVisits' => $maxVisits])),
ShortUrl::fromMeta(ShortUrlMeta::fromRawData(
['validSince' => $now, 'maxVisits' => $maxVisits, 'longUrl' => ''],
)),
[
'validSince' => $now->toAtomString(),
'validUntil' => null,
@ -56,8 +61,8 @@ class ShortUrlDataTransformerTest extends TestCase
],
];
yield 'both dates' => [
new ShortUrl('', ShortUrlMeta::fromRawData(
['validSince' => $now, 'validUntil' => $now->subDays(10)],
ShortUrl::fromMeta(ShortUrlMeta::fromRawData(
['validSince' => $now, 'validUntil' => $now->subDays(10), 'longUrl' => ''],
)),
[
'validSince' => $now->toAtomString(),
@ -66,8 +71,8 @@ class ShortUrlDataTransformerTest extends TestCase
],
];
yield 'everything' => [
new ShortUrl('', ShortUrlMeta::fromRawData(
['validSince' => $now, 'validUntil' => $now->subDays(5), 'maxVisits' => $maxVisits],
ShortUrl::fromMeta(ShortUrlMeta::fromRawData(
['validSince' => $now, 'validUntil' => $now->subDays(5), 'maxVisits' => $maxVisits, 'longUrl' => ''],
)),
[
'validSince' => $now->toAtomString(),

View file

@ -57,7 +57,7 @@ class VisitLocatorTest extends TestCase
): void {
$unlocatedVisits = map(
range(1, 200),
fn (int $i) => new Visit(new ShortUrl(sprintf('short_code_%s', $i)), Visitor::emptyInstance()),
fn (int $i) => new Visit(ShortUrl::withLongUrl(sprintf('short_code_%s', $i)), Visitor::emptyInstance()),
);
$findVisits = $this->mockRepoMethod($expectedRepoMethodName)->willReturn($unlocatedVisits);
@ -107,7 +107,7 @@ class VisitLocatorTest extends TestCase
bool $isNonLocatableAddress
): void {
$unlocatedVisits = [
new Visit(new ShortUrl('foo'), Visitor::emptyInstance()),
new Visit(ShortUrl::withLongUrl('foo'), Visitor::emptyInstance()),
];
$findVisits = $this->mockRepoMethod($expectedRepoMethodName)->willReturn($unlocatedVisits);

View file

@ -8,7 +8,7 @@ use Laminas\Diactoros\Response\JsonResponse;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Shlinkio\Shlink\Core\Exception\ValidationException;
use Shlinkio\Shlink\Core\Model\CreateShortUrlData;
use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
use Shlinkio\Shlink\Core\Service\UrlShortenerInterface;
use Shlinkio\Shlink\Core\Transformer\ShortUrlDataTransformer;
use Shlinkio\Shlink\Rest\Action\AbstractRestAction;
@ -26,12 +26,9 @@ abstract class AbstractCreateShortUrlAction extends AbstractRestAction
public function handle(Request $request): Response
{
$shortUrlData = $this->buildShortUrlData($request);
$longUrl = $shortUrlData->getLongUrl();
$tags = $shortUrlData->getTags();
$shortUrlMeta = $shortUrlData->getMeta();
$shortUrlMeta = $this->buildShortUrlData($request);
$shortUrl = $this->urlShortener->shorten($longUrl, $tags, $shortUrlMeta);
$shortUrl = $this->urlShortener->shorten($shortUrlMeta);
$transformer = new ShortUrlDataTransformer($this->domainConfig);
return new JsonResponse($transformer->transform($shortUrl));
@ -40,5 +37,5 @@ abstract class AbstractCreateShortUrlAction extends AbstractRestAction
/**
* @throws ValidationException
*/
abstract protected function buildShortUrlData(Request $request): CreateShortUrlData;
abstract protected function buildShortUrlData(Request $request): ShortUrlMeta;
}

View file

@ -6,7 +6,6 @@ namespace Shlinkio\Shlink\Rest\Action\ShortUrl;
use Psr\Http\Message\ServerRequestInterface as Request;
use Shlinkio\Shlink\Core\Exception\ValidationException;
use Shlinkio\Shlink\Core\Model\CreateShortUrlData;
use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
use Shlinkio\Shlink\Core\Validation\ShortUrlMetaInputFilter;
use Shlinkio\Shlink\Rest\Middleware\AuthenticationMiddleware;
@ -19,18 +18,11 @@ class CreateShortUrlAction extends AbstractCreateShortUrlAction
/**
* @throws ValidationException
*/
protected function buildShortUrlData(Request $request): CreateShortUrlData
protected function buildShortUrlData(Request $request): ShortUrlMeta
{
$payload = (array) $request->getParsedBody();
if (! isset($payload['longUrl'])) {
throw ValidationException::fromArray([
'longUrl' => 'A URL was not provided',
]);
}
$payload[ShortUrlMetaInputFilter::API_KEY] = AuthenticationMiddleware::apiKeyFromRequest($request);
$meta = ShortUrlMeta::fromRawData($payload);
return new CreateShortUrlData($payload['longUrl'], (array) ($payload['tags'] ?? []), $meta);
return ShortUrlMeta::fromRawData($payload);
}
}

View file

@ -5,8 +5,6 @@ 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\Model\CreateShortUrlData;
use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
use Shlinkio\Shlink\Core\Validation\ShortUrlMetaInputFilter;
use Shlinkio\Shlink\Rest\Middleware\AuthenticationMiddleware;
@ -16,22 +14,17 @@ class SingleStepCreateShortUrlAction extends AbstractCreateShortUrlAction
protected const ROUTE_PATH = '/short-urls/shorten';
protected const ROUTE_ALLOWED_METHODS = [self::METHOD_GET];
protected function buildShortUrlData(Request $request): CreateShortUrlData
protected function buildShortUrlData(Request $request): ShortUrlMeta
{
$query = $request->getQueryParams();
$longUrl = $query['longUrl'] ?? null;
if ($longUrl === null) {
throw ValidationException::fromArray([
'longUrl' => 'A URL was not provided',
]);
}
$apiKey = AuthenticationMiddleware::apiKeyFromRequest($request);
return new CreateShortUrlData($longUrl, [], ShortUrlMeta::fromRawData([
return ShortUrlMeta::fromRawData([
ShortUrlMetaInputFilter::LONG_URL => $longUrl,
ShortUrlMetaInputFilter::API_KEY => $apiKey,
// This will usually be null, unless this API key enforces one specific domain
ShortUrlMetaInputFilter::DOMAIN => $request->getAttribute(ShortUrlMetaInputFilter::DOMAIN),
]));
]);
}
}

View file

@ -212,10 +212,12 @@ class CreateShortUrlTest extends ApiTestCase
yield ['http://téstb.shlink.io']; // Redirects to http://tést.shlink.io
}
/** @test */
public function failsToCreateShortUrlWithInvalidLongUrl(): void
/**
* @test
* @dataProvider provideInvalidUrls
*/
public function failsToCreateShortUrlWithInvalidLongUrl(string $url): void
{
$url = 'https://this-has-to-be-invalid.com';
$expectedDetail = sprintf('Provided URL %s is invalid. Try with a different one.', $url);
[$statusCode, $payload] = $this->createShortUrl(['longUrl' => $url]);
@ -228,6 +230,25 @@ class CreateShortUrlTest extends ApiTestCase
self::assertEquals($url, $payload['url']);
}
public function provideInvalidUrls(): iterable
{
yield 'empty URL' => [''];
yield 'non-reachable URL' => ['https://this-has-to-be-invalid.com'];
}
/** @test */
public function failsToCreateShortUrlWithoutLongUrl(): void
{
$resp = $this->callApiWithKey(self::METHOD_POST, '/short-urls', [RequestOptions::JSON => []]);
$payload = $this->getJsonResponsePayload($resp);
self::assertEquals(self::STATUS_BAD_REQUEST, $resp->getStatusCode());
self::assertEquals(self::STATUS_BAD_REQUEST, $payload['status']);
self::assertEquals('INVALID_ARGUMENT', $payload['type']);
self::assertEquals('Provided data is not valid', $payload['detail']);
self::assertEquals('Invalid data', $payload['title']);
}
/** @test */
public function defaultDomainIsDroppedIfProvided(): void
{

View file

@ -27,44 +27,46 @@ class ShortUrlsFixture extends AbstractFixture implements DependentFixtureInterf
$authorApiKey = $this->getReference('author_api_key');
$abcShortUrl = $this->setShortUrlDate(
new ShortUrl('https://shlink.io', ShortUrlMeta::fromRawData(
['customSlug' => 'abc123', 'apiKey' => $authorApiKey],
ShortUrl::fromMeta(ShortUrlMeta::fromRawData(
['customSlug' => 'abc123', 'apiKey' => $authorApiKey, 'longUrl' => 'https://shlink.io'],
)),
'2018-05-01',
);
$manager->persist($abcShortUrl);
$defShortUrl = $this->setShortUrlDate(new ShortUrl(
'https://blog.alejandrocelaya.com/2017/12/09/acmailer-7-0-the-most-important-release-in-a-long-time/',
ShortUrlMeta::fromRawData(
['validSince' => Chronos::parse('2020-05-01'), 'customSlug' => 'def456', 'apiKey' => $authorApiKey],
),
), '2019-01-01 00:00:10');
$defShortUrl = $this->setShortUrlDate(ShortUrl::fromMeta(ShortUrlMeta::fromRawData([
'validSince' => Chronos::parse('2020-05-01'),
'customSlug' => 'def456',
'apiKey' => $authorApiKey,
'longUrl' =>
'https://blog.alejandrocelaya.com/2017/12/09/acmailer-7-0-the-most-important-release-in-a-long-time/',
])), '2019-01-01 00:00:10');
$manager->persist($defShortUrl);
$customShortUrl = $this->setShortUrlDate(new ShortUrl(
'https://shlink.io',
ShortUrlMeta::fromRawData(['customSlug' => 'custom', 'maxVisits' => 2, 'apiKey' => $authorApiKey]),
), '2019-01-01 00:00:20');
$customShortUrl = $this->setShortUrlDate(ShortUrl::fromMeta(ShortUrlMeta::fromRawData(
['customSlug' => 'custom', 'maxVisits' => 2, 'apiKey' => $authorApiKey, 'longUrl' => 'https://shlink.io'],
)), '2019-01-01 00:00:20');
$manager->persist($customShortUrl);
$ghiShortUrl = $this->setShortUrlDate(
new ShortUrl('https://shlink.io/documentation/', ShortUrlMeta::fromRawData(['customSlug' => 'ghi789'])),
ShortUrl::fromMeta(ShortUrlMeta::fromRawData(
['customSlug' => 'ghi789', 'longUrl' => 'https://shlink.io/documentation/'],
)),
'2018-05-01',
);
$manager->persist($ghiShortUrl);
$withDomainDuplicatingShortCode = $this->setShortUrlDate(new ShortUrl(
'https://blog.alejandrocelaya.com/2019/04/27/considerations-to-properly-use-open-source-software-projects/',
ShortUrlMeta::fromRawData(['domain' => 'example.com', 'customSlug' => 'ghi789']),
new PersistenceShortUrlRelationResolver($manager),
), '2019-01-01 00:00:30');
$withDomainDuplicatingShortCode = $this->setShortUrlDate(ShortUrl::fromMeta(ShortUrlMeta::fromRawData([
'domain' => 'example.com',
'customSlug' => 'ghi789',
'longUrl' => 'https://blog.alejandrocelaya.com/2019/04/27/considerations-to-properly-use-open-'
. 'source-software-projects/',
]), new PersistenceShortUrlRelationResolver($manager)), '2019-01-01 00:00:30');
$manager->persist($withDomainDuplicatingShortCode);
$withDomainAndSlugShortUrl = $this->setShortUrlDate(new ShortUrl(
'https://google.com',
ShortUrlMeta::fromRawData(['domain' => 'some-domain.com', 'customSlug' => 'custom-with-domain']),
), '2018-10-20');
$withDomainAndSlugShortUrl = $this->setShortUrlDate(ShortUrl::fromMeta(ShortUrlMeta::fromRawData(
['domain' => 'some-domain.com', 'customSlug' => 'custom-with-domain', 'longUrl' => 'https://google.com'],
)), '2018-10-20');
$manager->persist($withDomainAndSlugShortUrl);
$manager->flush();

View file

@ -39,27 +39,22 @@ class CreateShortUrlActionTest extends TestCase
}
/** @test */
public function missingLongUrlParamReturnsError(): void
{
$this->expectException(ValidationException::class);
$this->action->handle(new ServerRequest());
}
/**
* @test
* @dataProvider provideRequestBodies
*/
public function properShortcodeConversionReturnsData(array $body, array $expectedMeta): void
public function properShortcodeConversionReturnsData(): void
{
$apiKey = new ApiKey();
$shortUrl = new ShortUrl('');
$shortUrl = ShortUrl::createEmpty();
$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',
];
$expectedMeta['apiKey'] = $apiKey;
$shorten = $this->urlShortener->shorten(
Argument::type('string'),
Argument::type('array'),
ShortUrlMeta::fromRawData($expectedMeta),
)->willReturn($shortUrl);
$shorten = $this->urlShortener->shorten(ShortUrlMeta::fromRawData($expectedMeta))->willReturn($shortUrl);
$request = ServerRequestFactory::fromGlobals()->withParsedBody($body)->withAttribute(ApiKey::class, $apiKey);
@ -70,29 +65,13 @@ class CreateShortUrlActionTest extends TestCase
$shorten->shouldHaveBeenCalledOnce();
}
public function provideRequestBodies(): iterable
{
$fullMeta = [
'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',
];
yield 'no data' => [['longUrl' => 'http://www.domain.com/foo/bar'], []];
yield 'all data' => [$fullMeta, $fullMeta];
}
/**
* @test
* @dataProvider provideInvalidDomains
*/
public function anInvalidDomainReturnsError(string $domain): void
{
$shortUrl = new ShortUrl('');
$shortUrl = ShortUrl::createEmpty();
$urlToShortCode = $this->urlShortener->shorten(Argument::cetera())->willReturn($shortUrl);
$request = (new ServerRequest())->withParsedBody([

View file

@ -49,7 +49,7 @@ class EditShortUrlActionTest extends TestCase
'maxVisits' => 5,
]);
$updateMeta = $this->shortUrlService->updateMetadataByShortCode(Argument::cetera())->willReturn(
new ShortUrl(''),
ShortUrl::createEmpty(),
);
$resp = $this->action->handle($request);

View file

@ -45,7 +45,7 @@ class EditShortUrlTagsActionTest extends TestCase
new ShortUrlIdentifier($shortCode),
[],
Argument::type(ApiKey::class),
)->willReturn(new ShortUrl(''))
)->willReturn(ShortUrl::createEmpty())
->shouldBeCalledOnce();
$response = $this->action->handle(

View file

@ -35,7 +35,7 @@ class ResolveShortUrlActionTest extends TestCase
$shortCode = 'abc123';
$apiKey = new ApiKey();
$this->urlResolver->resolveShortUrl(new ShortUrlIdentifier($shortCode), $apiKey)->willReturn(
new ShortUrl('http://domain.com/foo/bar'),
ShortUrl::withLongUrl('http://domain.com/foo/bar'),
)->shouldBeCalledOnce();
$request = (new ServerRequest())->withAttribute('shortCode', $shortCode)->withAttribute(ApiKey::class, $apiKey);

View file

@ -5,13 +5,10 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\Rest\Action\ShortUrl;
use Laminas\Diactoros\ServerRequest;
use PHPUnit\Framework\Assert;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\Core\Entity\ShortUrl;
use Shlinkio\Shlink\Core\Exception\ValidationException;
use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
use Shlinkio\Shlink\Core\Service\UrlShortenerInterface;
use Shlinkio\Shlink\Rest\Action\ShortUrl\SingleStepCreateShortUrlAction;
@ -38,16 +35,6 @@ class SingleStepCreateShortUrlActionTest extends TestCase
);
}
/** @test */
public function errorResponseIsReturnedIfNoUrlIsProvided(): void
{
$request = new ServerRequest();
$this->expectException(ValidationException::class);
$this->action->handle($request);
}
/** @test */
public function properDataIsPassedWhenGeneratingShortCode(): void
{
@ -57,13 +44,8 @@ class SingleStepCreateShortUrlActionTest extends TestCase
'longUrl' => 'http://foobar.com',
])->withAttribute(ApiKey::class, $apiKey);
$generateShortCode = $this->urlShortener->shorten(
Argument::that(function (string $argument): bool {
Assert::assertEquals('http://foobar.com', $argument);
return true;
}),
[],
ShortUrlMeta::fromRawData(['apiKey' => $apiKey]),
)->willReturn(new ShortUrl(''));
ShortUrlMeta::fromRawData(['apiKey' => $apiKey, 'longUrl' => 'http://foobar.com']),
)->willReturn(ShortUrl::createEmpty());
$resp = $this->action->handle($request);