diff --git a/CHANGELOG.md b/CHANGELOG.md index 1cbab421..31cd05d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this ## [Unreleased] ### Added +* [#1612](https://github.com/shlinkio/shlink/issues/1612) Allowed to filter short URLs out of lists, when `validUntil` date is in the past or have reached their maximum amount of visits. + + This can be done by: + + * Providing `excludeMaxVisitsReached=true` and/or `excludePastValidUntil=true` to the `GET /short-urls` endpoint. + * Providing `--exclude-max-visits-reached` and/or `--exclude-past-valid-until` to the `short-urls:list` command. + * [#1616](https://github.com/shlinkio/shlink/issues/1616) Added support to import orphan visits when importing short URLs from another Shlink instance. * [#1519](https://github.com/shlinkio/shlink/issues/1519) Allowing to search short URLs by default domain. * [#1555](https://github.com/shlinkio/shlink/issues/1555) Added full support for PHP 8.2, pdating the dockr image to this version. diff --git a/composer.json b/composer.json index 762ead49..6a5bb317 100644 --- a/composer.json +++ b/composer.json @@ -45,7 +45,7 @@ "php-middleware/request-id": "^4.1", "pugx/shortid-php": "^1.1", "ramsey/uuid": "^4.5", - "shlinkio/shlink-common": "dev-main#7515008 as 5.2", + "shlinkio/shlink-common": "dev-main#f4101bc as 5.2", "shlinkio/shlink-config": "dev-main#96c81fb as 2.3", "shlinkio/shlink-event-dispatcher": "^2.6", "shlinkio/shlink-importer": "dev-main#c97662b as 5.0", diff --git a/docs/swagger/paths/v1_short-urls.json b/docs/swagger/paths/v1_short-urls.json index 2675ab61..05c5973a 100644 --- a/docs/swagger/paths/v1_short-urls.json +++ b/docs/swagger/paths/v1_short-urls.json @@ -97,6 +97,32 @@ "schema": { "type": "string" } + }, + { + "name": "excludeMaxVisitsReached", + "in": "query", + "description": "If true, short URLs which already reached their maximum amount of visits will be excluded.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "true", + "false" + ] + } + }, + { + "name": "excludePastValidUntil", + "in": "query", + "description": "If true, short URLs which validUntil date is on the past will be excluded.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "true", + "false" + ] + } } ], "security": [ diff --git a/module/CLI/src/Command/ShortUrl/ListShortUrlsCommand.php b/module/CLI/src/Command/ShortUrl/ListShortUrlsCommand.php index 11443abc..6e735221 100644 --- a/module/CLI/src/Command/ShortUrl/ListShortUrlsCommand.php +++ b/module/CLI/src/Command/ShortUrl/ListShortUrlsCommand.php @@ -77,6 +77,18 @@ class ListShortUrlsCommand extends Command InputOption::VALUE_NONE, 'If tags is provided, returns only short URLs having ALL tags.', ) + ->addOption( + 'exclude-max-visits-reached', + null, + InputOption::VALUE_NONE, + 'Excludes short URLs which reached their max amount of visits.', + ) + ->addOption( + 'exclude-past-valid-until', + null, + InputOption::VALUE_NONE, + 'Excludes short URLs which have a "validUntil" date in the past.', + ) ->addOption( 'order-by', 'o', @@ -133,6 +145,8 @@ class ListShortUrlsCommand extends Command ShortUrlsParamsInputFilter::ORDER_BY => $orderBy, ShortUrlsParamsInputFilter::START_DATE => $startDate?->toAtomString(), ShortUrlsParamsInputFilter::END_DATE => $endDate?->toAtomString(), + ShortUrlsParamsInputFilter::EXCLUDE_MAX_VISITS_REACHED => $input->getOption('exclude-max-visits-reached'), + ShortUrlsParamsInputFilter::EXCLUDE_PAST_VALID_UNTIL => $input->getOption('exclude-past-valid-until'), ]; if ($all) { diff --git a/module/CLI/test-cli/Command/ListShortUrlsTest.php b/module/CLI/test-cli/Command/ListShortUrlsTest.php index faa47a2f..c98573a5 100644 --- a/module/CLI/test-cli/Command/ListShortUrlsTest.php +++ b/module/CLI/test-cli/Command/ListShortUrlsTest.php @@ -61,6 +61,16 @@ class ListShortUrlsTest extends CliTestCase | custom-with-domain | | http://some-domain.com/custom-with-domain | https://google.com | 2018-10-20T00:00:00+00:00 | 0 | +--------------------+-------+-------------------------------------------+----------------------------- Page 1 of 1 -----------------------------------------------------------+---------------------------+--------------+ OUTPUT]; + yield 'expired excluded' => [['--exclude-max-visits-reached', '--exclude-past-valid-until'], <<shortUrlService->expects($this->once())->method('listShortUrls')->with( ShortUrlsParams::emptyInstance(), )->willReturn(new Paginator(new ArrayAdapter([ - ShortUrl::fromMeta(ShortUrlCreation::fromRawData([ + ShortUrl::create(ShortUrlCreation::fromRawData([ 'longUrl' => 'foo.com', 'tags' => ['foo', 'bar', 'baz'], 'apiKey' => $apiKey, diff --git a/module/Core/src/ShortUrl/Entity/ShortUrl.php b/module/Core/src/ShortUrl/Entity/ShortUrl.php index 7316c50b..c7f10b75 100644 --- a/module/Core/src/ShortUrl/Entity/ShortUrl.php +++ b/module/Core/src/ShortUrl/Entity/ShortUrl.php @@ -57,37 +57,37 @@ class ShortUrl extends AbstractEntity public static function createEmpty(): self { - return self::fromMeta(ShortUrlCreation::createEmpty()); + return self::create(ShortUrlCreation::createEmpty()); } public static function withLongUrl(string $longUrl): self { - return self::fromMeta(ShortUrlCreation::fromRawData([ShortUrlInputFilter::LONG_URL => $longUrl])); + return self::create(ShortUrlCreation::fromRawData([ShortUrlInputFilter::LONG_URL => $longUrl])); } - public static function fromMeta( - ShortUrlCreation $meta, + public static function create( + ShortUrlCreation $creation, ?ShortUrlRelationResolverInterface $relationResolver = null, ): self { $instance = new self(); $relationResolver = $relationResolver ?? new SimpleShortUrlRelationResolver(); - $instance->longUrl = $meta->getLongUrl(); + $instance->longUrl = $creation->getLongUrl(); $instance->dateCreated = Chronos::now(); $instance->visits = new ArrayCollection(); - $instance->tags = $relationResolver->resolveTags($meta->getTags()); - $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(); - $instance->title = $meta->getTitle(); - $instance->titleWasAutoResolved = $meta->titleWasAutoResolved(); - $instance->crawlable = $meta->isCrawlable(); - $instance->forwardQuery = $meta->forwardQuery(); + $instance->tags = $relationResolver->resolveTags($creation->getTags()); + $instance->validSince = $creation->getValidSince(); + $instance->validUntil = $creation->getValidUntil(); + $instance->maxVisits = $creation->getMaxVisits(); + $instance->customSlugWasProvided = $creation->hasCustomSlug(); + $instance->shortCodeLength = $creation->getShortCodeLength(); + $instance->shortCode = $creation->getCustomSlug() ?? generateRandomShortCode($instance->shortCodeLength); + $instance->domain = $relationResolver->resolveDomain($creation->getDomain()); + $instance->authorApiKey = $creation->getApiKey(); + $instance->title = $creation->getTitle(); + $instance->titleWasAutoResolved = $creation->titleWasAutoResolved(); + $instance->crawlable = $creation->isCrawlable(); + $instance->forwardQuery = $creation->forwardQuery(); return $instance; } @@ -109,7 +109,7 @@ class ShortUrl extends AbstractEntity $meta[ShortUrlInputFilter::CUSTOM_SLUG] = $url->shortCode; } - $instance = self::fromMeta(ShortUrlCreation::fromRawData($meta), $relationResolver); + $instance = self::create(ShortUrlCreation::fromRawData($meta), $relationResolver); $instance->importSource = $url->source->value; $instance->importOriginalShortCode = $url->shortCode; diff --git a/module/Core/src/ShortUrl/Model/ShortUrlsParams.php b/module/Core/src/ShortUrl/Model/ShortUrlsParams.php index 14e88132..e053a283 100644 --- a/module/Core/src/ShortUrl/Model/ShortUrlsParams.php +++ b/module/Core/src/ShortUrl/Model/ShortUrlsParams.php @@ -24,6 +24,8 @@ final class ShortUrlsParams public readonly array $tags, public readonly Ordering $orderBy, public readonly ?DateRange $dateRange, + public readonly bool $excludeMaxVisitsReached, + public readonly bool $excludePastValidUntil, public readonly TagsMode $tagsMode = TagsMode::ANY, ) { } @@ -55,6 +57,8 @@ final class ShortUrlsParams normalizeOptionalDate($inputFilter->getValue(ShortUrlsParamsInputFilter::START_DATE)), normalizeOptionalDate($inputFilter->getValue(ShortUrlsParamsInputFilter::END_DATE)), ), + excludeMaxVisitsReached: $inputFilter->getValue(ShortUrlsParamsInputFilter::EXCLUDE_MAX_VISITS_REACHED), + excludePastValidUntil: $inputFilter->getValue(ShortUrlsParamsInputFilter::EXCLUDE_PAST_VALID_UNTIL), tagsMode: self::resolveTagsMode($inputFilter->getValue(ShortUrlsParamsInputFilter::TAGS_MODE)), ); } diff --git a/module/Core/src/ShortUrl/Model/TagsMode.php b/module/Core/src/ShortUrl/Model/TagsMode.php index 593d6d83..01cdcc3b 100644 --- a/module/Core/src/ShortUrl/Model/TagsMode.php +++ b/module/Core/src/ShortUrl/Model/TagsMode.php @@ -4,8 +4,15 @@ declare(strict_types=1); namespace Shlinkio\Shlink\Core\ShortUrl\Model; +use function Functional\map; + enum TagsMode: string { case ANY = 'any'; case ALL = 'all'; + + public static function values(): array + { + return map(self::cases(), static fn (TagsMode $mode) => $mode->value); + } } diff --git a/module/Core/src/ShortUrl/Model/Validation/ShortUrlsParamsInputFilter.php b/module/Core/src/ShortUrl/Model/Validation/ShortUrlsParamsInputFilter.php index a5301f21..3bdea8e2 100644 --- a/module/Core/src/ShortUrl/Model/Validation/ShortUrlsParamsInputFilter.php +++ b/module/Core/src/ShortUrl/Model/Validation/ShortUrlsParamsInputFilter.php @@ -23,6 +23,8 @@ class ShortUrlsParamsInputFilter extends InputFilter public const ITEMS_PER_PAGE = 'itemsPerPage'; public const TAGS_MODE = 'tagsMode'; public const ORDER_BY = 'orderBy'; + public const EXCLUDE_MAX_VISITS_REACHED = 'excludeMaxVisitsReached'; + public const EXCLUDE_PAST_VALID_UNTIL = 'excludePastValidUntil'; public function __construct(array $data) { @@ -44,11 +46,14 @@ class ShortUrlsParamsInputFilter extends InputFilter $tagsMode = $this->createInput(self::TAGS_MODE, false); $tagsMode->getValidatorChain()->attach(new InArray([ - 'haystack' => [TagsMode::ALL->value, TagsMode::ANY->value], + 'haystack' => TagsMode::values(), 'strict' => InArray::COMPARE_STRICT, ])); $this->add($tagsMode); $this->add($this->createOrderByInput(self::ORDER_BY, ShortUrlsParams::ORDERABLE_FIELDS)); + + $this->add($this->createBooleanInput(self::EXCLUDE_MAX_VISITS_REACHED, false)); + $this->add($this->createBooleanInput(self::EXCLUDE_PAST_VALID_UNTIL, false)); } } diff --git a/module/Core/src/ShortUrl/Persistence/ShortUrlsCountFiltering.php b/module/Core/src/ShortUrl/Persistence/ShortUrlsCountFiltering.php index 2d1b1f21..906adc63 100644 --- a/module/Core/src/ShortUrl/Persistence/ShortUrlsCountFiltering.php +++ b/module/Core/src/ShortUrl/Persistence/ShortUrlsCountFiltering.php @@ -21,6 +21,8 @@ class ShortUrlsCountFiltering public readonly array $tags = [], public readonly ?TagsMode $tagsMode = null, public readonly ?DateRange $dateRange = null, + public readonly bool $excludeMaxVisitsReached = false, + public readonly bool $excludePastValidUntil = false, public readonly ?ApiKey $apiKey = null, ?string $defaultDomain = null, ) { @@ -37,6 +39,8 @@ class ShortUrlsCountFiltering $params->tags, $params->tagsMode, $params->dateRange, + $params->excludeMaxVisitsReached, + $params->excludePastValidUntil, $apiKey, $defaultDomain, ); diff --git a/module/Core/src/ShortUrl/Persistence/ShortUrlsListFiltering.php b/module/Core/src/ShortUrl/Persistence/ShortUrlsListFiltering.php index dd7eb0aa..db8b9a70 100644 --- a/module/Core/src/ShortUrl/Persistence/ShortUrlsListFiltering.php +++ b/module/Core/src/ShortUrl/Persistence/ShortUrlsListFiltering.php @@ -20,10 +20,21 @@ class ShortUrlsListFiltering extends ShortUrlsCountFiltering array $tags = [], ?TagsMode $tagsMode = null, ?DateRange $dateRange = null, + bool $excludeMaxVisitsReached = false, + bool $excludePastValidUntil = false, ?ApiKey $apiKey = null, ?string $defaultDomain = null, ) { - parent::__construct($searchTerm, $tags, $tagsMode, $dateRange, $apiKey, $defaultDomain); + parent::__construct( + $searchTerm, + $tags, + $tagsMode, + $dateRange, + $excludeMaxVisitsReached, + $excludePastValidUntil, + $apiKey, + $defaultDomain, + ); } public static function fromLimitsAndParams( @@ -41,6 +52,8 @@ class ShortUrlsListFiltering extends ShortUrlsCountFiltering $params->tags, $params->tagsMode, $params->dateRange, + $params->excludeMaxVisitsReached, + $params->excludePastValidUntil, $apiKey, $defaultDomain, ); diff --git a/module/Core/src/ShortUrl/Repository/ShortUrlRepository.php b/module/Core/src/ShortUrl/Repository/ShortUrlRepository.php index ec2f7a94..6acf1c2c 100644 --- a/module/Core/src/ShortUrl/Repository/ShortUrlRepository.php +++ b/module/Core/src/ShortUrl/Repository/ShortUrlRepository.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Shlinkio\Shlink\Core\ShortUrl\Repository; +use Cake\Chronos\Chronos; use Doctrine\DBAL\LockMode; use Doctrine\DBAL\Platforms\PostgreSQLPlatform; use Doctrine\ORM\Query\Expr\Join; @@ -11,18 +12,19 @@ use Doctrine\ORM\QueryBuilder; use Happyr\DoctrineSpecification\Repository\EntitySpecificationRepository; use Happyr\DoctrineSpecification\Specification\Specification; use Shlinkio\Shlink\Common\Doctrine\Type\ChronosDateTimeType; -use Shlinkio\Shlink\Core\Model\Ordering; 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\TagsMode; use Shlinkio\Shlink\Core\ShortUrl\Persistence\ShortUrlsCountFiltering; use Shlinkio\Shlink\Core\ShortUrl\Persistence\ShortUrlsListFiltering; +use Shlinkio\Shlink\Core\Visit\Entity\Visit; use Shlinkio\Shlink\Importer\Model\ImportedShlinkUrl; use function array_column; use function count; use function Functional\contains; +use function sprintf; class ShortUrlRepository extends EntitySpecificationRepository implements ShortUrlRepositoryInterface { @@ -37,36 +39,37 @@ class ShortUrlRepository extends EntitySpecificationRepository implements ShortU ->setFirstResult($filtering->offset); // In case the ordering has been specified, the query could be more complex. Process it - if ($filtering->orderBy->hasOrderField()) { - return $this->processOrderByForList($qb, $filtering->orderBy); + $this->processOrderByForList($qb, $filtering); + + $result = $qb->getQuery()->getResult(); + if ($filtering->orderBy->field === 'visits') { + return array_column($result, 0); } - // With no explicit order by, fallback to dateCreated-DESC - return $qb->orderBy('s.dateCreated', 'DESC')->getQuery()->getResult(); + return $result; } - private function processOrderByForList(QueryBuilder $qb, Ordering $orderBy): array + private function processOrderByForList(QueryBuilder $qb, ShortUrlsListFiltering $filtering): void { - $fieldName = $orderBy->field; - $order = $orderBy->direction; + // With no explicit order by, fallback to dateCreated-DESC + if (! $filtering->orderBy->hasOrderField()) { + $qb->orderBy('s.dateCreated', 'DESC'); + return; + } + + $fieldName = $filtering->orderBy->field; + $order = $filtering->orderBy->direction; if ($fieldName === 'visits') { // FIXME This query is inefficient. // Diagnostic: It might need to use a sub-query, as done with the tags list query. - $qb->addSelect('COUNT(DISTINCT v) AS totalVisits') + $qb->addSelect('COUNT(DISTINCT v)') ->leftJoin('s.visits', 'v') ->groupBy('s') - ->orderBy('totalVisits', $order); - - return array_column($qb->getQuery()->getResult(), 0); - } - - $orderableFields = ['longUrl', 'shortCode', 'dateCreated', 'title']; - if (contains($orderableFields, $fieldName)) { + ->orderBy('COUNT(DISTINCT v)', $order); + } elseif (contains(['longUrl', 'shortCode', 'dateCreated', 'title'], $fieldName)) { $qb->orderBy('s.' . $fieldName, $order); } - - return $qb->getQuery()->getResult(); } public function countList(ShortUrlsCountFiltering $filtering): int @@ -134,6 +137,25 @@ class ShortUrlRepository extends EntitySpecificationRepository implements ShortU : $this->joinAllTags($qb, $tags); } + if ($filtering->excludeMaxVisitsReached) { + $qb->andWhere($qb->expr()->orX( + $qb->expr()->isNull('s.maxVisits'), + $qb->expr()->gt( + 's.maxVisits', + sprintf('(SELECT COUNT(innerV.id) FROM %s as innerV WHERE innerV.shortUrl=s)', Visit::class), + ), + )); + } + + if ($filtering->excludePastValidUntil) { + $qb + ->andWhere($qb->expr()->orX( + $qb->expr()->isNull('s.validUntil'), + $qb->expr()->gte('s.validUntil', ':minValidUntil'), + )) + ->setParameter('minValidUntil', Chronos::now()->toDateTimeString()); + } + $this->applySpecification($qb, $filtering->apiKey?->spec(), 's'); return $qb; diff --git a/module/Core/src/ShortUrl/UrlShortener.php b/module/Core/src/ShortUrl/UrlShortener.php index 4f979921..d3f54650 100644 --- a/module/Core/src/ShortUrl/UrlShortener.php +++ b/module/Core/src/ShortUrl/UrlShortener.php @@ -44,7 +44,7 @@ class UrlShortener implements UrlShortenerInterface /** @var ShortUrl $newShortUrl */ $newShortUrl = $this->em->wrapInTransaction(function () use ($meta) { - $shortUrl = ShortUrl::fromMeta($meta, $this->relationResolver); + $shortUrl = ShortUrl::create($meta, $this->relationResolver); $this->verifyShortCodeUniqueness($meta, $shortUrl); $this->em->persist($shortUrl); diff --git a/module/Core/test-db/Domain/Repository/DomainRepositoryTest.php b/module/Core/test-db/Domain/Repository/DomainRepositoryTest.php index 17f65abc..c96d70ff 100644 --- a/module/Core/test-db/Domain/Repository/DomainRepositoryTest.php +++ b/module/Core/test-db/Domain/Repository/DomainRepositoryTest.php @@ -129,7 +129,7 @@ class DomainRepositoryTest extends DatabaseTestCase private function createShortUrl(Domain $domain, ?ApiKey $apiKey = null): ShortUrl { - return ShortUrl::fromMeta( + return ShortUrl::create( ShortUrlCreation::fromRawData( ['domain' => $domain->getAuthority(), 'apiKey' => $apiKey, 'longUrl' => 'foo'], ), diff --git a/module/Core/test-db/ShortUrl/Repository/ShortUrlRepositoryTest.php b/module/Core/test-db/ShortUrl/Repository/ShortUrlRepositoryTest.php index e41eee15..dc1d6420 100644 --- a/module/Core/test-db/ShortUrl/Repository/ShortUrlRepositoryTest.php +++ b/module/Core/test-db/ShortUrl/Repository/ShortUrlRepositoryTest.php @@ -43,15 +43,15 @@ class ShortUrlRepositoryTest extends DatabaseTestCase /** @test */ public function findOneWithDomainFallbackReturnsProperData(): void { - $regularOne = ShortUrl::fromMeta(ShortUrlCreation::fromRawData(['customSlug' => 'foo', 'longUrl' => 'foo'])); + $regularOne = ShortUrl::create(ShortUrlCreation::fromRawData(['customSlug' => 'foo', 'longUrl' => 'foo'])); $this->getEntityManager()->persist($regularOne); - $withDomain = ShortUrl::fromMeta(ShortUrlCreation::fromRawData( + $withDomain = ShortUrl::create(ShortUrlCreation::fromRawData( ['domain' => 'example.com', 'customSlug' => 'domain-short-code', 'longUrl' => 'foo'], )); $this->getEntityManager()->persist($withDomain); - $withDomainDuplicatingRegular = ShortUrl::fromMeta(ShortUrlCreation::fromRawData( + $withDomainDuplicatingRegular = ShortUrl::create(ShortUrlCreation::fromRawData( ['domain' => 'doma.in', 'customSlug' => 'foo', 'longUrl' => 'foo_with_domain'], )); $this->getEntityManager()->persist($withDomainDuplicatingRegular); @@ -101,7 +101,7 @@ class ShortUrlRepositoryTest extends DatabaseTestCase /** @test */ public function findListProperlyFiltersResult(): void { - $foo = ShortUrl::fromMeta( + $foo = ShortUrl::create( ShortUrlCreation::fromRawData(['longUrl' => 'foo', 'tags' => ['bar']]), $this->relationResolver, ); @@ -197,27 +197,27 @@ class ShortUrlRepositoryTest extends DatabaseTestCase /** @test */ public function findListReturnsOnlyThoseWithMatchingTags(): void { - $shortUrl1 = ShortUrl::fromMeta(ShortUrlCreation::fromRawData([ + $shortUrl1 = ShortUrl::create(ShortUrlCreation::fromRawData([ 'longUrl' => 'foo1', 'tags' => ['foo', 'bar'], ]), $this->relationResolver); $this->getEntityManager()->persist($shortUrl1); - $shortUrl2 = ShortUrl::fromMeta(ShortUrlCreation::fromRawData([ + $shortUrl2 = ShortUrl::create(ShortUrlCreation::fromRawData([ 'longUrl' => 'foo2', 'tags' => ['foo', 'baz'], ]), $this->relationResolver); $this->getEntityManager()->persist($shortUrl2); - $shortUrl3 = ShortUrl::fromMeta(ShortUrlCreation::fromRawData([ + $shortUrl3 = ShortUrl::create(ShortUrlCreation::fromRawData([ 'longUrl' => 'foo3', 'tags' => ['foo'], ]), $this->relationResolver); $this->getEntityManager()->persist($shortUrl3); - $shortUrl4 = ShortUrl::fromMeta(ShortUrlCreation::fromRawData([ + $shortUrl4 = ShortUrl::create(ShortUrlCreation::fromRawData([ 'longUrl' => 'foo4', 'tags' => ['bar', 'baz'], ]), $this->relationResolver); $this->getEntityManager()->persist($shortUrl4); - $shortUrl5 = ShortUrl::fromMeta(ShortUrlCreation::fromRawData([ + $shortUrl5 = ShortUrl::create(ShortUrlCreation::fromRawData([ 'longUrl' => 'foo5', 'tags' => ['bar', 'baz'], ]), $this->relationResolver); @@ -306,17 +306,17 @@ class ShortUrlRepositoryTest extends DatabaseTestCase /** @test */ public function findListReturnsOnlyThoseWithMatchingDomains(): void { - $shortUrl1 = ShortUrl::fromMeta(ShortUrlCreation::fromRawData([ + $shortUrl1 = ShortUrl::create(ShortUrlCreation::fromRawData([ 'longUrl' => 'foo1', 'domain' => null, ]), $this->relationResolver); $this->getEntityManager()->persist($shortUrl1); - $shortUrl2 = ShortUrl::fromMeta(ShortUrlCreation::fromRawData([ + $shortUrl2 = ShortUrl::create(ShortUrlCreation::fromRawData([ 'longUrl' => 'foo2', 'domain' => null, ]), $this->relationResolver); $this->getEntityManager()->persist($shortUrl2); - $shortUrl3 = ShortUrl::fromMeta(ShortUrlCreation::fromRawData([ + $shortUrl3 = ShortUrl::create(ShortUrlCreation::fromRawData([ 'longUrl' => 'foo3', 'domain' => 'another.com', ]), $this->relationResolver); @@ -339,15 +339,63 @@ class ShortUrlRepositoryTest extends DatabaseTestCase self::assertCount(0, $this->repo->findList($buildFiltering('no results'))); } + /** @test */ + public function findListReturnsOnlyThoseWithoutExcludedUrls(): void + { + $shortUrl1 = ShortUrl::create(ShortUrlCreation::fromRawData([ + 'longUrl' => 'foo1', + 'validUntil' => Chronos::now()->addDays(1)->toAtomString(), + 'maxVisits' => 100, + ]), $this->relationResolver); + $this->getEntityManager()->persist($shortUrl1); + $shortUrl2 = ShortUrl::create(ShortUrlCreation::fromRawData([ + 'longUrl' => 'foo2', + 'validUntil' => Chronos::now()->subDays(1)->toAtomString(), + ]), $this->relationResolver); + $this->getEntityManager()->persist($shortUrl2); + $shortUrl3 = ShortUrl::create(ShortUrlCreation::fromRawData([ + 'longUrl' => 'foo3', + ]), $this->relationResolver); + $this->getEntityManager()->persist($shortUrl3); + $shortUrl4 = ShortUrl::create(ShortUrlCreation::fromRawData([ + 'longUrl' => 'foo4', + 'maxVisits' => 3, + ]), $this->relationResolver); + $this->getEntityManager()->persist($shortUrl4); + $this->getEntityManager()->persist(Visit::forValidShortUrl($shortUrl4, Visitor::emptyInstance())); + $this->getEntityManager()->persist(Visit::forValidShortUrl($shortUrl4, Visitor::emptyInstance())); + $this->getEntityManager()->persist(Visit::forValidShortUrl($shortUrl4, Visitor::emptyInstance())); + + $this->getEntityManager()->flush(); + + $filtering = static fn (bool $excludeMaxVisitsReached, bool $excludePastValidUntil) => + new ShortUrlsListFiltering( + null, + null, + Ordering::emptyInstance(), + excludeMaxVisitsReached: $excludeMaxVisitsReached, + excludePastValidUntil: $excludePastValidUntil, + ); + + self::assertCount(4, $this->repo->findList($filtering(false, false))); + self::assertEquals(4, $this->repo->countList($filtering(false, false))); + self::assertCount(3, $this->repo->findList($filtering(true, false))); + self::assertEquals(3, $this->repo->countList($filtering(true, false))); + self::assertCount(3, $this->repo->findList($filtering(false, true))); + self::assertEquals(3, $this->repo->countList($filtering(false, true))); + self::assertCount(2, $this->repo->findList($filtering(true, true))); + self::assertEquals(2, $this->repo->countList($filtering(true, true))); + } + /** @test */ public function shortCodeIsInUseLooksForShortUrlInProperSetOfTables(): void { - $shortUrlWithoutDomain = ShortUrl::fromMeta( + $shortUrlWithoutDomain = ShortUrl::create( ShortUrlCreation::fromRawData(['customSlug' => 'my-cool-slug', 'longUrl' => 'foo']), ); $this->getEntityManager()->persist($shortUrlWithoutDomain); - $shortUrlWithDomain = ShortUrl::fromMeta( + $shortUrlWithDomain = ShortUrl::create( ShortUrlCreation::fromRawData(['domain' => 'doma.in', 'customSlug' => 'another-slug', 'longUrl' => 'foo']), ); $this->getEntityManager()->persist($shortUrlWithDomain); @@ -371,12 +419,12 @@ class ShortUrlRepositoryTest extends DatabaseTestCase /** @test */ public function findOneLooksForShortUrlInProperSetOfTables(): void { - $shortUrlWithoutDomain = ShortUrl::fromMeta( + $shortUrlWithoutDomain = ShortUrl::create( ShortUrlCreation::fromRawData(['customSlug' => 'my-cool-slug', 'longUrl' => 'foo']), ); $this->getEntityManager()->persist($shortUrlWithoutDomain); - $shortUrlWithDomain = ShortUrl::fromMeta( + $shortUrlWithDomain = ShortUrl::create( ShortUrlCreation::fromRawData(['domain' => 'doma.in', 'customSlug' => 'another-slug', 'longUrl' => 'foo']), ); $this->getEntityManager()->persist($shortUrlWithDomain); @@ -417,29 +465,29 @@ class ShortUrlRepositoryTest extends DatabaseTestCase $start = Chronos::parse('2020-03-05 20:18:30'); $end = Chronos::parse('2021-03-05 20:18:30'); - $shortUrl = ShortUrl::fromMeta( + $shortUrl = ShortUrl::create( ShortUrlCreation::fromRawData(['validSince' => $start, 'longUrl' => 'foo', 'tags' => ['foo', 'bar']]), $this->relationResolver, ); $this->getEntityManager()->persist($shortUrl); - $shortUrl2 = ShortUrl::fromMeta(ShortUrlCreation::fromRawData(['validUntil' => $end, 'longUrl' => 'bar'])); + $shortUrl2 = ShortUrl::create(ShortUrlCreation::fromRawData(['validUntil' => $end, 'longUrl' => 'bar'])); $this->getEntityManager()->persist($shortUrl2); - $shortUrl3 = ShortUrl::fromMeta( + $shortUrl3 = ShortUrl::create( ShortUrlCreation::fromRawData(['validSince' => $start, 'validUntil' => $end, 'longUrl' => 'baz']), ); $this->getEntityManager()->persist($shortUrl3); - $shortUrl4 = ShortUrl::fromMeta( + $shortUrl4 = ShortUrl::create( ShortUrlCreation::fromRawData(['customSlug' => 'custom', 'validUntil' => $end, 'longUrl' => 'foo']), ); $this->getEntityManager()->persist($shortUrl4); - $shortUrl5 = ShortUrl::fromMeta(ShortUrlCreation::fromRawData(['maxVisits' => 3, 'longUrl' => 'foo'])); + $shortUrl5 = ShortUrl::create(ShortUrlCreation::fromRawData(['maxVisits' => 3, 'longUrl' => 'foo'])); $this->getEntityManager()->persist($shortUrl5); - $shortUrl6 = ShortUrl::fromMeta(ShortUrlCreation::fromRawData(['domain' => 'doma.in', 'longUrl' => 'foo'])); + $shortUrl6 = ShortUrl::create(ShortUrlCreation::fromRawData(['domain' => 'doma.in', 'longUrl' => 'foo'])); $this->getEntityManager()->persist($shortUrl6); $this->getEntityManager()->flush(); @@ -489,15 +537,15 @@ class ShortUrlRepositoryTest extends DatabaseTestCase ['validSince' => $start, 'maxVisits' => 50, 'longUrl' => 'foo', 'tags' => $tags], ); - $shortUrl1 = ShortUrl::fromMeta($meta, $this->relationResolver); + $shortUrl1 = ShortUrl::create($meta, $this->relationResolver); $this->getEntityManager()->persist($shortUrl1); $this->getEntityManager()->flush(); - $shortUrl2 = ShortUrl::fromMeta($meta, $this->relationResolver); + $shortUrl2 = ShortUrl::create($meta, $this->relationResolver); $this->getEntityManager()->persist($shortUrl2); $this->getEntityManager()->flush(); - $shortUrl3 = ShortUrl::fromMeta($meta, $this->relationResolver); + $shortUrl3 = ShortUrl::create($meta, $this->relationResolver); $this->getEntityManager()->persist($shortUrl3); $this->getEntityManager()->flush(); @@ -531,7 +579,7 @@ class ShortUrlRepositoryTest extends DatabaseTestCase $adminApiKey = ApiKey::create(); $this->getEntityManager()->persist($adminApiKey); - $shortUrl = ShortUrl::fromMeta(ShortUrlCreation::fromRawData([ + $shortUrl = ShortUrl::create(ShortUrlCreation::fromRawData([ 'validSince' => $start, 'apiKey' => $apiKey, 'domain' => $rightDomain->getAuthority(), @@ -540,7 +588,7 @@ class ShortUrlRepositoryTest extends DatabaseTestCase ]), $this->relationResolver); $this->getEntityManager()->persist($shortUrl); - $nonDomainShortUrl = ShortUrl::fromMeta(ShortUrlCreation::fromRawData([ + $nonDomainShortUrl = ShortUrl::create(ShortUrlCreation::fromRawData([ 'apiKey' => $apiKey, 'longUrl' => 'non-domain', ]), $this->relationResolver); @@ -659,7 +707,7 @@ class ShortUrlRepositoryTest extends DatabaseTestCase /** @test */ public function findCrawlableShortCodesReturnsExpectedResult(): void { - $createShortUrl = fn (bool $crawlable) => ShortUrl::fromMeta( + $createShortUrl = fn (bool $crawlable) => ShortUrl::create( ShortUrlCreation::fromRawData(['crawlable' => $crawlable, 'longUrl' => 'foo.com']), ); diff --git a/module/Core/test-db/Tag/Repository/TagRepositoryTest.php b/module/Core/test-db/Tag/Repository/TagRepositoryTest.php index b71577b9..ce0efff9 100644 --- a/module/Core/test-db/Tag/Repository/TagRepositoryTest.php +++ b/module/Core/test-db/Tag/Repository/TagRepositoryTest.php @@ -77,22 +77,22 @@ class TagRepositoryTest extends DatabaseTestCase ['longUrl' => '', 'tags' => $tags, 'apiKey' => $apiKey], ); - $shortUrl = ShortUrl::fromMeta($metaWithTags($firstUrlTags, $apiKey), $this->relationResolver); + $shortUrl = ShortUrl::create($metaWithTags($firstUrlTags, $apiKey), $this->relationResolver); $this->getEntityManager()->persist($shortUrl); $this->getEntityManager()->persist(Visit::forValidShortUrl($shortUrl, Visitor::emptyInstance())); $this->getEntityManager()->persist(Visit::forValidShortUrl($shortUrl, Visitor::emptyInstance())); $this->getEntityManager()->persist(Visit::forValidShortUrl($shortUrl, Visitor::emptyInstance())); - $shortUrl2 = ShortUrl::fromMeta($metaWithTags($secondUrlTags, null), $this->relationResolver); + $shortUrl2 = ShortUrl::create($metaWithTags($secondUrlTags, null), $this->relationResolver); $this->getEntityManager()->persist($shortUrl2); $this->getEntityManager()->persist(Visit::forValidShortUrl($shortUrl2, Visitor::emptyInstance())); // One of the tags has two extra short URLs, but with no visits $this->getEntityManager()->persist( - ShortUrl::fromMeta($metaWithTags(['bar'], null), $this->relationResolver), + ShortUrl::create($metaWithTags(['bar'], null), $this->relationResolver), ); $this->getEntityManager()->persist( - ShortUrl::fromMeta($metaWithTags(['bar'], $apiKey), $this->relationResolver), + ShortUrl::create($metaWithTags(['bar'], $apiKey), $this->relationResolver), ); $this->getEntityManager()->flush(); @@ -222,13 +222,13 @@ class TagRepositoryTest extends DatabaseTestCase [$firstUrlTags, $secondUrlTags] = array_chunk($names, 3); - $shortUrl = ShortUrl::fromMeta( + $shortUrl = ShortUrl::create( ShortUrlCreation::fromRawData(['apiKey' => $authorApiKey, 'longUrl' => '', 'tags' => $firstUrlTags]), $this->relationResolver, ); $this->getEntityManager()->persist($shortUrl); - $shortUrl2 = ShortUrl::fromMeta( + $shortUrl2 = ShortUrl::create( ShortUrlCreation::fromRawData( ['domain' => $domain->getAuthority(), 'longUrl' => '', 'tags' => $secondUrlTags], ), diff --git a/module/Core/test-db/Visit/Repository/VisitRepositoryTest.php b/module/Core/test-db/Visit/Repository/VisitRepositoryTest.php index b69190e5..da475832 100644 --- a/module/Core/test-db/Visit/Repository/VisitRepositoryTest.php +++ b/module/Core/test-db/Visit/Repository/VisitRepositoryTest.php @@ -313,7 +313,7 @@ class VisitRepositoryTest extends DatabaseTestCase $apiKey1 = ApiKey::fromMeta(ApiKeyMeta::withRoles(RoleDefinition::forAuthoredShortUrls())); $this->getEntityManager()->persist($apiKey1); - $shortUrl = ShortUrl::fromMeta( + $shortUrl = ShortUrl::create( ShortUrlCreation::fromRawData(['apiKey' => $apiKey1, 'domain' => $domain->getAuthority(), 'longUrl' => '']), $this->relationResolver, ); @@ -322,11 +322,11 @@ class VisitRepositoryTest extends DatabaseTestCase $apiKey2 = ApiKey::fromMeta(ApiKeyMeta::withRoles(RoleDefinition::forAuthoredShortUrls())); $this->getEntityManager()->persist($apiKey2); - $shortUrl2 = ShortUrl::fromMeta(ShortUrlCreation::fromRawData(['apiKey' => $apiKey2, 'longUrl' => ''])); + $shortUrl2 = ShortUrl::create(ShortUrlCreation::fromRawData(['apiKey' => $apiKey2, 'longUrl' => ''])); $this->getEntityManager()->persist($shortUrl2); $this->createVisitsForShortUrl($shortUrl2, 5); - $shortUrl3 = ShortUrl::fromMeta( + $shortUrl3 = ShortUrl::create( ShortUrlCreation::fromRawData(['apiKey' => $apiKey2, 'domain' => $domain->getAuthority(), 'longUrl' => '']), $this->relationResolver, ); @@ -365,7 +365,7 @@ class VisitRepositoryTest extends DatabaseTestCase /** @test */ public function findOrphanVisitsReturnsExpectedResult(): void { - $shortUrl = ShortUrl::fromMeta(ShortUrlCreation::fromRawData(['longUrl' => ''])); + $shortUrl = ShortUrl::create(ShortUrlCreation::fromRawData(['longUrl' => ''])); $this->getEntityManager()->persist($shortUrl); $this->createVisitsForShortUrl($shortUrl, 7); @@ -414,7 +414,7 @@ class VisitRepositoryTest extends DatabaseTestCase /** @test */ public function countOrphanVisitsReturnsExpectedResult(): void { - $shortUrl = ShortUrl::fromMeta(ShortUrlCreation::fromRawData(['longUrl' => ''])); + $shortUrl = ShortUrl::create(ShortUrlCreation::fromRawData(['longUrl' => ''])); $this->getEntityManager()->persist($shortUrl); $this->createVisitsForShortUrl($shortUrl, 7); @@ -451,15 +451,15 @@ class VisitRepositoryTest extends DatabaseTestCase /** @test */ public function findNonOrphanVisitsReturnsExpectedResult(): void { - $shortUrl = ShortUrl::fromMeta(ShortUrlCreation::fromRawData(['longUrl' => '1'])); + $shortUrl = ShortUrl::create(ShortUrlCreation::fromRawData(['longUrl' => '1'])); $this->getEntityManager()->persist($shortUrl); $this->createVisitsForShortUrl($shortUrl, 7); - $shortUrl2 = ShortUrl::fromMeta(ShortUrlCreation::fromRawData(['longUrl' => '2'])); + $shortUrl2 = ShortUrl::create(ShortUrlCreation::fromRawData(['longUrl' => '2'])); $this->getEntityManager()->persist($shortUrl2); $this->createVisitsForShortUrl($shortUrl2, 4); - $shortUrl3 = ShortUrl::fromMeta(ShortUrlCreation::fromRawData(['longUrl' => '3'])); + $shortUrl3 = ShortUrl::create(ShortUrlCreation::fromRawData(['longUrl' => '3'])); $this->getEntityManager()->persist($shortUrl3); $this->createVisitsForShortUrl($shortUrl3, 10); @@ -517,7 +517,7 @@ class VisitRepositoryTest extends DatabaseTestCase array $tags = [], ?ApiKey $apiKey = null, ): array { - $shortUrl = ShortUrl::fromMeta(ShortUrlCreation::fromRawData([ + $shortUrl = ShortUrl::create(ShortUrlCreation::fromRawData([ ShortUrlInputFilter::LONG_URL => '', ShortUrlInputFilter::TAGS => $tags, ShortUrlInputFilter::API_KEY => $apiKey, @@ -529,7 +529,7 @@ class VisitRepositoryTest extends DatabaseTestCase $this->createVisitsForShortUrl($shortUrl); if ($withDomain !== false) { - $shortUrlWithDomain = ShortUrl::fromMeta(ShortUrlCreation::fromRawData([ + $shortUrlWithDomain = ShortUrl::create(ShortUrlCreation::fromRawData([ 'customSlug' => $shortCode, 'domain' => $domain, 'longUrl' => '', diff --git a/module/Core/test/EventDispatcher/PublishingUpdatesGeneratorTest.php b/module/Core/test/EventDispatcher/PublishingUpdatesGeneratorTest.php index 99ebb820..3ac690d0 100644 --- a/module/Core/test/EventDispatcher/PublishingUpdatesGeneratorTest.php +++ b/module/Core/test/EventDispatcher/PublishingUpdatesGeneratorTest.php @@ -35,7 +35,7 @@ class PublishingUpdatesGeneratorTest extends TestCase */ public function visitIsProperlySerializedIntoUpdate(string $method, string $expectedTopic, ?string $title): void { - $shortUrl = ShortUrl::fromMeta(ShortUrlCreation::fromRawData([ + $shortUrl = ShortUrl::create(ShortUrlCreation::fromRawData([ 'customSlug' => 'foo', 'longUrl' => '', 'title' => $title, @@ -114,7 +114,7 @@ class PublishingUpdatesGeneratorTest extends TestCase /** @test */ public function shortUrlIsProperlySerializedIntoUpdate(): void { - $shortUrl = ShortUrl::fromMeta(ShortUrlCreation::fromRawData([ + $shortUrl = ShortUrl::create(ShortUrlCreation::fromRawData([ 'customSlug' => 'foo', 'longUrl' => '', 'title' => 'The title', diff --git a/module/Core/test/EventDispatcher/RabbitMq/NotifyVisitToRabbitMqTest.php b/module/Core/test/EventDispatcher/RabbitMq/NotifyVisitToRabbitMqTest.php index 0def544e..8b7b392c 100644 --- a/module/Core/test/EventDispatcher/RabbitMq/NotifyVisitToRabbitMqTest.php +++ b/module/Core/test/EventDispatcher/RabbitMq/NotifyVisitToRabbitMqTest.php @@ -98,7 +98,7 @@ class NotifyVisitToRabbitMqTest extends TestCase yield 'orphan visit' => [Visit::forBasePath($visitor), ['newOrphanVisitUpdate']]; yield 'non-orphan visit' => [ Visit::forValidShortUrl( - ShortUrl::fromMeta(ShortUrlCreation::fromRawData([ + ShortUrl::create(ShortUrlCreation::fromRawData([ 'longUrl' => 'foo', 'customSlug' => 'bar', ])), diff --git a/module/Core/test/ShortUrl/Entity/ShortUrlTest.php b/module/Core/test/ShortUrl/Entity/ShortUrlTest.php index d874dbf5..026778ae 100644 --- a/module/Core/test/ShortUrl/Entity/ShortUrlTest.php +++ b/module/Core/test/ShortUrl/Entity/ShortUrlTest.php @@ -38,7 +38,7 @@ class ShortUrlTest extends TestCase public function provideInvalidShortUrls(): iterable { yield 'with custom slug' => [ - ShortUrl::fromMeta(ShortUrlCreation::fromRawData(['customSlug' => 'custom-slug', 'longUrl' => ''])), + ShortUrl::create(ShortUrlCreation::fromRawData(['customSlug' => 'custom-slug', 'longUrl' => ''])), 'The short code cannot be regenerated on ShortUrls where a custom slug was provided.', ]; yield 'already persisted' => [ @@ -77,7 +77,7 @@ class ShortUrlTest extends TestCase */ public function shortCodesHaveExpectedLength(?int $length, int $expectedLength): void { - $shortUrl = ShortUrl::fromMeta(ShortUrlCreation::fromRawData( + $shortUrl = ShortUrl::create(ShortUrlCreation::fromRawData( [ShortUrlInputFilter::SHORT_CODE_LENGTH => $length, 'longUrl' => ''], )); diff --git a/module/Core/test/ShortUrl/Helper/ShortUrlRedirectionBuilderTest.php b/module/Core/test/ShortUrl/Helper/ShortUrlRedirectionBuilderTest.php index 1a077c19..cb94a9f1 100644 --- a/module/Core/test/ShortUrl/Helper/ShortUrlRedirectionBuilderTest.php +++ b/module/Core/test/ShortUrl/Helper/ShortUrlRedirectionBuilderTest.php @@ -30,7 +30,7 @@ class ShortUrlRedirectionBuilderTest extends TestCase ?string $extraPath, ?bool $forwardQuery, ): void { - $shortUrl = ShortUrl::fromMeta(ShortUrlCreation::fromRawData([ + $shortUrl = ShortUrl::create(ShortUrlCreation::fromRawData([ 'longUrl' => 'https://domain.com/foo/bar?some=thing', 'forwardQuery' => $forwardQuery, ])); diff --git a/module/Core/test/ShortUrl/Helper/ShortUrlStringifierTest.php b/module/Core/test/ShortUrl/Helper/ShortUrlStringifierTest.php index d46fbf92..b6d5a123 100644 --- a/module/Core/test/ShortUrl/Helper/ShortUrlStringifierTest.php +++ b/module/Core/test/ShortUrl/Helper/ShortUrlStringifierTest.php @@ -28,7 +28,7 @@ class ShortUrlStringifierTest extends TestCase public function provideConfigAndShortUrls(): iterable { - $shortUrlWithShortCode = fn (string $shortCode, ?string $domain = null) => ShortUrl::fromMeta( + $shortUrlWithShortCode = fn (string $shortCode, ?string $domain = null) => ShortUrl::create( ShortUrlCreation::fromRawData([ 'longUrl' => '', 'customSlug' => $shortCode, diff --git a/module/Core/test/ShortUrl/Paginator/Adapter/ShortUrlRepositoryAdapterTest.php b/module/Core/test/ShortUrl/Paginator/Adapter/ShortUrlRepositoryAdapterTest.php index aa8efbd5..e271448c 100644 --- a/module/Core/test/ShortUrl/Paginator/Adapter/ShortUrlRepositoryAdapterTest.php +++ b/module/Core/test/ShortUrl/Paginator/Adapter/ShortUrlRepositoryAdapterTest.php @@ -74,7 +74,7 @@ class ShortUrlRepositoryAdapterTest extends TestCase $dateRange = $params->dateRange; $this->repo->expects($this->once())->method('countList')->with( - new ShortUrlsCountFiltering($searchTerm, $tags, TagsMode::ANY, $dateRange, $apiKey), + new ShortUrlsCountFiltering($searchTerm, $tags, TagsMode::ANY, $dateRange, apiKey: $apiKey), ); $adapter->getNbResults(); } diff --git a/module/Core/test/ShortUrl/ShortUrlResolverTest.php b/module/Core/test/ShortUrl/ShortUrlResolverTest.php index 1e86ec1c..9c2bcab3 100644 --- a/module/Core/test/ShortUrl/ShortUrlResolverTest.php +++ b/module/Core/test/ShortUrl/ShortUrlResolverTest.php @@ -114,7 +114,7 @@ class ShortUrlResolverTest extends TestCase $now = Chronos::now(); yield 'maxVisits reached' => [(function () { - $shortUrl = ShortUrl::fromMeta(ShortUrlCreation::fromRawData(['maxVisits' => 3, 'longUrl' => ''])); + $shortUrl = ShortUrl::create(ShortUrlCreation::fromRawData(['maxVisits' => 3, 'longUrl' => ''])); $shortUrl->setVisits(new ArrayCollection(map( range(0, 4), fn () => Visit::forValidShortUrl($shortUrl, Visitor::emptyInstance()), @@ -122,14 +122,14 @@ class ShortUrlResolverTest extends TestCase return $shortUrl; })()]; - yield 'future validSince' => [ShortUrl::fromMeta(ShortUrlCreation::fromRawData( + yield 'future validSince' => [ShortUrl::create(ShortUrlCreation::fromRawData( ['validSince' => $now->addMonth()->toAtomString(), 'longUrl' => ''], ))]; - yield 'past validUntil' => [ShortUrl::fromMeta(ShortUrlCreation::fromRawData( + yield 'past validUntil' => [ShortUrl::create(ShortUrlCreation::fromRawData( ['validUntil' => $now->subMonth()->toAtomString(), 'longUrl' => ''], ))]; yield 'mixed' => [(function () use ($now) { - $shortUrl = ShortUrl::fromMeta(ShortUrlCreation::fromRawData([ + $shortUrl = ShortUrl::create(ShortUrlCreation::fromRawData([ 'maxVisits' => 3, 'validUntil' => $now->subMonth()->toAtomString(), 'longUrl' => '', diff --git a/module/Core/test/ShortUrl/Transformer/ShortUrlDataTransformerTest.php b/module/Core/test/ShortUrl/Transformer/ShortUrlDataTransformerTest.php index 376e3d03..c9df4e38 100644 --- a/module/Core/test/ShortUrl/Transformer/ShortUrlDataTransformerTest.php +++ b/module/Core/test/ShortUrl/Transformer/ShortUrlDataTransformerTest.php @@ -43,7 +43,7 @@ class ShortUrlDataTransformerTest extends TestCase 'validUntil' => null, 'maxVisits' => null, ]]; - yield 'max visits only' => [ShortUrl::fromMeta(ShortUrlCreation::fromRawData([ + yield 'max visits only' => [ShortUrl::create(ShortUrlCreation::fromRawData([ 'maxVisits' => $maxVisits, 'longUrl' => '', ])), [ @@ -52,7 +52,7 @@ class ShortUrlDataTransformerTest extends TestCase 'maxVisits' => $maxVisits, ]]; yield 'max visits and valid since' => [ - ShortUrl::fromMeta(ShortUrlCreation::fromRawData( + ShortUrl::create(ShortUrlCreation::fromRawData( ['validSince' => $now, 'maxVisits' => $maxVisits, 'longUrl' => ''], )), [ @@ -62,7 +62,7 @@ class ShortUrlDataTransformerTest extends TestCase ], ]; yield 'both dates' => [ - ShortUrl::fromMeta(ShortUrlCreation::fromRawData( + ShortUrl::create(ShortUrlCreation::fromRawData( ['validSince' => $now, 'validUntil' => $now->subDays(10), 'longUrl' => ''], )), [ @@ -72,7 +72,7 @@ class ShortUrlDataTransformerTest extends TestCase ], ]; yield 'everything' => [ - ShortUrl::fromMeta(ShortUrlCreation::fromRawData( + ShortUrl::create(ShortUrlCreation::fromRawData( ['validSince' => $now, 'validUntil' => $now->subDays(5), 'maxVisits' => $maxVisits, 'longUrl' => ''], )), [ diff --git a/module/Core/test/ShortUrl/UrlShortenerTest.php b/module/Core/test/ShortUrl/UrlShortenerTest.php index 4c9e8646..d59de634 100644 --- a/module/Core/test/ShortUrl/UrlShortenerTest.php +++ b/module/Core/test/ShortUrl/UrlShortenerTest.php @@ -107,17 +107,17 @@ class UrlShortenerTest extends TestCase ), ShortUrl::withLongUrl($url)]; yield [ ShortUrlCreation::fromRawData(['findIfExists' => true, 'longUrl' => $url, 'tags' => ['foo', 'bar']]), - ShortUrl::fromMeta(ShortUrlCreation::fromRawData(['longUrl' => $url, 'tags' => ['foo', 'bar']])), + ShortUrl::create(ShortUrlCreation::fromRawData(['longUrl' => $url, 'tags' => ['foo', 'bar']])), ]; yield [ ShortUrlCreation::fromRawData(['findIfExists' => true, 'maxVisits' => 3, 'longUrl' => $url]), - ShortUrl::fromMeta(ShortUrlCreation::fromRawData(['maxVisits' => 3, 'longUrl' => $url])), + ShortUrl::create(ShortUrlCreation::fromRawData(['maxVisits' => 3, 'longUrl' => $url])), ]; yield [ ShortUrlCreation::fromRawData( ['findIfExists' => true, 'validSince' => Chronos::parse('2017-01-01'), 'longUrl' => $url], ), - ShortUrl::fromMeta( + ShortUrl::create( ShortUrlCreation::fromRawData(['validSince' => Chronos::parse('2017-01-01'), 'longUrl' => $url]), ), ]; @@ -125,13 +125,13 @@ class UrlShortenerTest extends TestCase ShortUrlCreation::fromRawData( ['findIfExists' => true, 'validUntil' => Chronos::parse('2017-01-01'), 'longUrl' => $url], ), - ShortUrl::fromMeta( + ShortUrl::create( ShortUrlCreation::fromRawData(['validUntil' => Chronos::parse('2017-01-01'), 'longUrl' => $url]), ), ]; yield [ ShortUrlCreation::fromRawData(['findIfExists' => true, 'domain' => 'example.com', 'longUrl' => $url]), - ShortUrl::fromMeta(ShortUrlCreation::fromRawData(['domain' => 'example.com', 'longUrl' => $url])), + ShortUrl::create(ShortUrlCreation::fromRawData(['domain' => 'example.com', 'longUrl' => $url])), ]; yield [ ShortUrlCreation::fromRawData([ @@ -141,7 +141,7 @@ class UrlShortenerTest extends TestCase 'longUrl' => $url, 'tags' => ['baz', 'foo', 'bar'], ]), - ShortUrl::fromMeta(ShortUrlCreation::fromRawData([ + ShortUrl::create(ShortUrlCreation::fromRawData([ 'validUntil' => Chronos::parse('2017-01-01'), 'maxVisits' => 4, 'longUrl' => $url, diff --git a/module/Rest/test-api/Action/ListShortUrlsTest.php b/module/Rest/test-api/Action/ListShortUrlsTest.php index b28a0b5d..63664a6d 100644 --- a/module/Rest/test-api/Action/ListShortUrlsTest.php +++ b/module/Rest/test-api/Action/ListShortUrlsTest.php @@ -22,7 +22,7 @@ class ListShortUrlsTest extends ApiTestCase 'meta' => [ 'validSince' => null, 'validUntil' => null, - 'maxVisits' => null, + 'maxVisits' => 2, ], 'domain' => null, 'title' => 'My cool title', @@ -38,7 +38,7 @@ class ListShortUrlsTest extends ApiTestCase 'tags' => [], 'meta' => [ 'validSince' => null, - 'validUntil' => null, + 'validUntil' => '2020-05-01T00:00:00+00:00', 'maxVisits' => null, ], 'domain' => null, @@ -147,6 +147,20 @@ class ListShortUrlsTest extends ApiTestCase self::SHORT_URL_SHLINK_WITH_TITLE, self::SHORT_URL_DOCS, ], 'valid_api_key']; + yield [['excludePastValidUntil' => 'true'], [ + self::SHORT_URL_CUSTOM_DOMAIN, + self::SHORT_URL_CUSTOM_SLUG, + self::SHORT_URL_META, + self::SHORT_URL_CUSTOM_SLUG_AND_DOMAIN, + self::SHORT_URL_SHLINK_WITH_TITLE, + ], 'valid_api_key']; + yield [['excludeMaxVisitsReached' => 'true'], [ + self::SHORT_URL_CUSTOM_DOMAIN, + self::SHORT_URL_CUSTOM_SLUG, + self::SHORT_URL_META, + self::SHORT_URL_CUSTOM_SLUG_AND_DOMAIN, + self::SHORT_URL_DOCS, + ], 'valid_api_key']; yield [['orderBy' => 'shortCode'], [ self::SHORT_URL_SHLINK_WITH_TITLE, self::SHORT_URL_CUSTOM_SLUG, diff --git a/module/Rest/test-api/Fixtures/ShortUrlsFixture.php b/module/Rest/test-api/Fixtures/ShortUrlsFixture.php index eadf60ee..9a876463 100644 --- a/module/Rest/test-api/Fixtures/ShortUrlsFixture.php +++ b/module/Rest/test-api/Fixtures/ShortUrlsFixture.php @@ -29,19 +29,20 @@ class ShortUrlsFixture extends AbstractFixture implements DependentFixtureInterf $authorApiKey = $this->getReference('author_api_key'); $abcShortUrl = $this->setShortUrlDate( - ShortUrl::fromMeta(ShortUrlCreation::fromRawData([ + ShortUrl::create(ShortUrlCreation::fromRawData([ 'customSlug' => 'abc123', 'apiKey' => $authorApiKey, 'longUrl' => 'https://shlink.io', 'tags' => ['foo'], 'title' => 'My cool title', 'crawlable' => true, + 'maxVisits' => 2, ]), $relationResolver), '2018-05-01', ); $manager->persist($abcShortUrl); - $defShortUrl = $this->setShortUrlDate(ShortUrl::fromMeta(ShortUrlCreation::fromRawData([ + $defShortUrl = $this->setShortUrlDate(ShortUrl::create(ShortUrlCreation::fromRawData([ 'validSince' => Chronos::parse('2020-05-01'), 'customSlug' => 'def456', 'apiKey' => $authorApiKey, @@ -51,7 +52,7 @@ class ShortUrlsFixture extends AbstractFixture implements DependentFixtureInterf ]), $relationResolver), '2019-01-01 00:00:10'); $manager->persist($defShortUrl); - $customShortUrl = $this->setShortUrlDate(ShortUrl::fromMeta(ShortUrlCreation::fromRawData([ + $customShortUrl = $this->setShortUrlDate(ShortUrl::create(ShortUrlCreation::fromRawData([ 'customSlug' => 'custom', 'maxVisits' => 2, 'apiKey' => $authorApiKey, @@ -61,14 +62,16 @@ class ShortUrlsFixture extends AbstractFixture implements DependentFixtureInterf $manager->persist($customShortUrl); $ghiShortUrl = $this->setShortUrlDate( - ShortUrl::fromMeta(ShortUrlCreation::fromRawData( - ['customSlug' => 'ghi789', 'longUrl' => 'https://shlink.io/documentation/'], - )), + ShortUrl::create(ShortUrlCreation::fromRawData([ + 'customSlug' => 'ghi789', + 'longUrl' => 'https://shlink.io/documentation/', + 'validUntil' => Chronos::parse('2020-05-01'), // In the past + ])), '2018-05-01', ); $manager->persist($ghiShortUrl); - $withDomainDuplicatingShortCode = $this->setShortUrlDate(ShortUrl::fromMeta(ShortUrlCreation::fromRawData([ + $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-' @@ -77,7 +80,7 @@ class ShortUrlsFixture extends AbstractFixture implements DependentFixtureInterf ]), $relationResolver), '2019-01-01 00:00:30'); $manager->persist($withDomainDuplicatingShortCode); - $withDomainAndSlugShortUrl = $this->setShortUrlDate(ShortUrl::fromMeta(ShortUrlCreation::fromRawData( + $withDomainAndSlugShortUrl = $this->setShortUrlDate(ShortUrl::create(ShortUrlCreation::fromRawData( ['domain' => 'some-domain.com', 'customSlug' => 'custom-with-domain', 'longUrl' => 'https://google.com'], )), '2018-10-20'); $manager->persist($withDomainAndSlugShortUrl);