mirror of
https://github.com/shlinkio/shlink.git
synced 2025-02-17 15:59:56 +03:00
Merge pull request #1003 from acelaya-forks/feature/title
Feature/title
This commit is contained in:
commit
a8b424003c
41 changed files with 529 additions and 114 deletions
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
|
@ -189,7 +189,7 @@ jobs:
|
|||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: Start database server
|
||||
run: docker-compose -f docker-compose.yml -f docker-compose.ci.yml up -d shlink_db
|
||||
run: docker-compose -f docker-compose.yml -f docker-compose.ci.yml up -d shlink_db_postgres
|
||||
- name: Use PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
|
|
|
@ -7,6 +7,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this
|
|||
## [Unreleased]
|
||||
### Added
|
||||
* [#856](https://github.com/shlinkio/shlink/issues/856) Added PHP 8.0 support.
|
||||
* [#941](https://github.com/shlinkio/shlink/issues/856) Added support to provide a title for every short URL.
|
||||
|
||||
The title can also be automatically resolved from the long URL, when no title was explicitly provided, but this option needs to be opted in.
|
||||
|
||||
### Changed
|
||||
* [#977](https://github.com/shlinkio/shlink/issues/977) Migrated from `laminas/laminas-paginator` to `pagerfanta/core` to handle pagination.
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
#!/usr/bin/env sh
|
||||
export APP_ENV=test
|
||||
export DB_DRIVER=mysql
|
||||
export DB_DRIVER=postgres
|
||||
export TEST_ENV=api
|
||||
|
||||
# Try to stop server just in case it hanged in last execution
|
||||
|
|
|
@ -50,8 +50,8 @@
|
|||
"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",
|
||||
"shlinkio/shlink-installer": "^5.3",
|
||||
"shlinkio/shlink-importer": "dev-main#b6fc81f as 2.2",
|
||||
"shlinkio/shlink-installer": "dev-develop#1ed5ac8 as 5.4",
|
||||
"shlinkio/shlink-ip-geolocation": "^1.5",
|
||||
"symfony/console": "^5.1",
|
||||
"symfony/filesystem": "^5.1",
|
||||
|
@ -64,7 +64,7 @@
|
|||
"devster/ubench": "^2.1",
|
||||
"dms/phpunit-arraysubset-asserts": "^0.2.1",
|
||||
"eaglewu/swoole-ide-helper": "dev-master",
|
||||
"infection/infection": "^0.20.2",
|
||||
"infection/infection": "^0.21.0",
|
||||
"phpspec/prophecy-phpunit": "^2.0",
|
||||
"phpstan/phpstan": "^0.12.64",
|
||||
"phpunit/php-code-coverage": "^9.2",
|
||||
|
|
|
@ -40,6 +40,7 @@ return [
|
|||
Option\UrlShortener\IpAnonymizationConfigOption::class,
|
||||
Option\UrlShortener\RedirectStatusCodeConfigOption::class,
|
||||
Option\UrlShortener\RedirectCacheLifeTimeConfigOption::class,
|
||||
Option\UrlShortener\AutoResolveTitlesConfigOption::class,
|
||||
],
|
||||
|
||||
'installation_commands' => [
|
||||
|
|
|
@ -19,6 +19,7 @@ return [
|
|||
'default_short_codes_length' => DEFAULT_SHORT_CODES_LENGTH,
|
||||
'redirect_status_code' => DEFAULT_REDIRECT_STATUS_CODE,
|
||||
'redirect_cache_lifetime' => DEFAULT_REDIRECT_CACHE_LIFETIME,
|
||||
'auto_resolve_titles' => false,
|
||||
],
|
||||
|
||||
];
|
||||
|
|
44
data/migrations/Version20210202181026.php
Normal file
44
data/migrations/Version20210202181026.php
Normal file
|
@ -0,0 +1,44 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20210202181026 extends AbstractMigration
|
||||
{
|
||||
private const TITLE = 'title';
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$shortUrls = $schema->getTable('short_urls');
|
||||
$this->skipIf($shortUrls->hasColumn(self::TITLE));
|
||||
|
||||
$shortUrls->addColumn(self::TITLE, Types::STRING, [
|
||||
'notnull' => false,
|
||||
'length' => 512,
|
||||
]);
|
||||
$shortUrls->addColumn('title_was_auto_resolved', Types::BOOLEAN, [
|
||||
'default' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$shortUrls = $schema->getTable('short_urls');
|
||||
$this->skipIf(! $shortUrls->hasColumn(self::TITLE));
|
||||
$shortUrls->dropColumn(self::TITLE);
|
||||
$shortUrls->dropColumn('title_was_auto_resolved');
|
||||
}
|
||||
|
||||
/**
|
||||
* @fixme Workaround for https://github.com/doctrine/migrations/issues/1104
|
||||
*/
|
||||
public function isTransactional(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -125,6 +125,7 @@ return [
|
|||
'default_short_codes_length' => $helper->getDefaultShortCodesLength(),
|
||||
'redirect_status_code' => (int) env('REDIRECT_STATUS_CODE', DEFAULT_REDIRECT_STATUS_CODE),
|
||||
'redirect_cache_lifetime' => (int) env('REDIRECT_CACHE_LIFETIME', DEFAULT_REDIRECT_CACHE_LIFETIME),
|
||||
'auto_resolve_titles' => (bool) env('AUTO_RESOLVE_TITLES', false),
|
||||
],
|
||||
|
||||
'not_found_redirects' => $helper->getNotFoundRedirectsConfig(),
|
||||
|
|
|
@ -34,7 +34,13 @@
|
|||
},
|
||||
"domain": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"description": "The domain in which the short URL was created. Null if it belongs to default domain."
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"description": "A descriptive title of the short URL."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -64,7 +64,9 @@
|
|||
"dateCreated-ASC",
|
||||
"dateCreated-DESC",
|
||||
"visits-ASC",
|
||||
"visits-DESC"
|
||||
"visits-DESC",
|
||||
"title-ASC",
|
||||
"title-DESC"
|
||||
]
|
||||
}
|
||||
},
|
||||
|
@ -137,7 +139,8 @@
|
|||
"validUntil": null,
|
||||
"maxVisits": 100
|
||||
},
|
||||
"domain": null
|
||||
"domain": null,
|
||||
"title": "Welcome to Steam"
|
||||
},
|
||||
{
|
||||
"shortCode": "12Kb3",
|
||||
|
@ -153,7 +156,8 @@
|
|||
"validUntil": null,
|
||||
"maxVisits": null
|
||||
},
|
||||
"domain": null
|
||||
"domain": null,
|
||||
"title": null
|
||||
},
|
||||
{
|
||||
"shortCode": "123bA",
|
||||
|
@ -167,7 +171,8 @@
|
|||
"validUntil": null,
|
||||
"maxVisits": null
|
||||
},
|
||||
"domain": "example.com"
|
||||
"domain": "example.com",
|
||||
"title": null
|
||||
}
|
||||
],
|
||||
"pagination": {
|
||||
|
@ -264,6 +269,10 @@
|
|||
"validateUrl": {
|
||||
"description": "Tells if the long URL should or should not be validated as a reachable URL. If not provided, it will fall back to app-level config",
|
||||
"type": "boolean"
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"description": "A descriptive title of the short URL."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -73,7 +73,8 @@
|
|||
"validUntil": null,
|
||||
"maxVisits": 100
|
||||
},
|
||||
"domain": null
|
||||
"domain": null,
|
||||
"title": null
|
||||
},
|
||||
"text/plain": "https://doma.in/abc123"
|
||||
}
|
||||
|
|
|
@ -53,7 +53,8 @@
|
|||
"validUntil": null,
|
||||
"maxVisits": 100
|
||||
},
|
||||
"domain": null
|
||||
"domain": null,
|
||||
"title": null
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -118,15 +119,18 @@
|
|||
},
|
||||
"validSince": {
|
||||
"description": "The date (in ISO-8601 format) from which this short code will be valid",
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"validUntil": {
|
||||
"description": "The date (in ISO-8601 format) until which this short code will be valid",
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"maxVisits": {
|
||||
"description": "The maximum number of allowed visits for this short code",
|
||||
"type": "number"
|
||||
"type": "number",
|
||||
"nullable": true
|
||||
},
|
||||
"validateUrl": {
|
||||
"description": "Tells if the long URL (if provided) should or should not be validated as a reachable URL. If not provided, it will fall back to app-level config",
|
||||
|
@ -138,6 +142,11 @@
|
|||
"type": "string"
|
||||
},
|
||||
"description": "The list of tags to set to the short URL."
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"description": "A descriptive title of the short URL.",
|
||||
"nullable": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -174,7 +183,8 @@
|
|||
"validUntil": null,
|
||||
"maxVisits": 100
|
||||
},
|
||||
"domain": null
|
||||
"domain": null,
|
||||
"title": "Shlink - The URL shortener"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -19,11 +19,9 @@ use Symfony\Component\Console\Input\InputOption;
|
|||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
use function array_flip;
|
||||
use function array_intersect_key;
|
||||
use function array_values;
|
||||
use function count;
|
||||
use function array_pad;
|
||||
use function explode;
|
||||
use function Functional\map;
|
||||
use function implode;
|
||||
use function sprintf;
|
||||
|
||||
|
@ -32,12 +30,16 @@ class ListShortUrlsCommand extends AbstractWithDateRangeCommand
|
|||
use PagerfantaUtilsTrait;
|
||||
|
||||
public const NAME = 'short-url:list';
|
||||
private const COLUMNS_WHITELIST = [
|
||||
private const COLUMNS_TO_SHOW = [
|
||||
'shortCode',
|
||||
'title',
|
||||
'shortUrl',
|
||||
'longUrl',
|
||||
'dateCreated',
|
||||
'visitsCount',
|
||||
];
|
||||
private const COLUMNS_TO_SHOW_WITH_TAGS = [
|
||||
...self::COLUMNS_TO_SHOW,
|
||||
'tags',
|
||||
];
|
||||
|
||||
|
@ -79,7 +81,8 @@ class ListShortUrlsCommand extends AbstractWithDateRangeCommand
|
|||
'order-by',
|
||||
'o',
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'The field from which we want to order by. Pass ASC or DESC separated by a comma.',
|
||||
'The field from which you want to order by. '
|
||||
. 'Define ordering dir by passing ASC or DESC after "," or "-".',
|
||||
)
|
||||
->addOptionWithDeprecatedFallback(
|
||||
'show-tags',
|
||||
|
@ -153,21 +156,20 @@ class ListShortUrlsCommand extends AbstractWithDateRangeCommand
|
|||
{
|
||||
$result = $this->shortUrlService->listShortUrls($params);
|
||||
|
||||
$headers = ['Short code', 'Short URL', 'Long URL', 'Date created', 'Visits count'];
|
||||
$headers = ['Short code', 'Title', 'Short URL', 'Long URL', 'Date created', 'Visits count'];
|
||||
if ($showTags) {
|
||||
$headers[] = 'Tags';
|
||||
}
|
||||
|
||||
$rows = [];
|
||||
foreach ($result as $row) {
|
||||
$columnsToShow = $showTags ? self::COLUMNS_TO_SHOW_WITH_TAGS : self::COLUMNS_TO_SHOW;
|
||||
$shortUrl = $this->transformer->transform($row);
|
||||
if ($showTags) {
|
||||
$shortUrl['tags'] = implode(', ', $shortUrl['tags']);
|
||||
} else {
|
||||
unset($shortUrl['tags']);
|
||||
}
|
||||
|
||||
$rows[] = array_values(array_intersect_key($shortUrl, array_flip(self::COLUMNS_WHITELIST)));
|
||||
$rows[] = map($columnsToShow, fn (string $prop) => $shortUrl[$prop]);
|
||||
}
|
||||
|
||||
ShlinkTable::fromOutput($output)->render($headers, $rows, $all ? null : $this->formatCurrentPageMessage(
|
||||
|
@ -178,17 +180,14 @@ class ListShortUrlsCommand extends AbstractWithDateRangeCommand
|
|||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array|string|null
|
||||
*/
|
||||
private function processOrderBy(InputInterface $input)
|
||||
private function processOrderBy(InputInterface $input): ?string
|
||||
{
|
||||
$orderBy = $this->getOptionWithDeprecatedFallback($input, 'order-by');
|
||||
if (empty($orderBy)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$orderBy = explode(',', $orderBy);
|
||||
return count($orderBy) === 1 ? $orderBy[0] : [$orderBy[0] => $orderBy[1]];
|
||||
[$field, $dir] = array_pad(explode(',', $orderBy), 2, null);
|
||||
return $dir === null ? $field : sprintf('%s-%s', $field, $dir);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -44,6 +44,7 @@ return [
|
|||
|
||||
ShortUrl\Resolver\PersistenceShortUrlRelationResolver::class => ConfigAbstractFactory::class,
|
||||
ShortUrl\Helper\ShortUrlStringifier::class => ConfigAbstractFactory::class,
|
||||
ShortUrl\Helper\ShortUrlTitleResolutionHelper::class => ConfigAbstractFactory::class,
|
||||
ShortUrl\Transformer\ShortUrlDataTransformer::class => ConfigAbstractFactory::class,
|
||||
|
||||
Mercure\MercureUpdatesGenerator::class => ConfigAbstractFactory::class,
|
||||
|
@ -69,7 +70,7 @@ return [
|
|||
Options\UrlShortenerOptions::class => ['config.url_shortener'],
|
||||
|
||||
Service\UrlShortener::class => [
|
||||
Util\UrlValidator::class,
|
||||
ShortUrl\Helper\ShortUrlTitleResolutionHelper::class,
|
||||
'em',
|
||||
ShortUrl\Resolver\PersistenceShortUrlRelationResolver::class,
|
||||
Service\ShortUrl\ShortCodeHelper::class,
|
||||
|
@ -82,7 +83,7 @@ return [
|
|||
Service\ShortUrlService::class => [
|
||||
'em',
|
||||
Service\ShortUrl\ShortUrlResolver::class,
|
||||
Util\UrlValidator::class,
|
||||
ShortUrl\Helper\ShortUrlTitleResolutionHelper::class,
|
||||
ShortUrl\Resolver\PersistenceShortUrlRelationResolver::class,
|
||||
],
|
||||
Visit\VisitLocator::class => ['em'],
|
||||
|
@ -122,6 +123,7 @@ return [
|
|||
|
||||
ShortUrl\Resolver\PersistenceShortUrlRelationResolver::class => ['em'],
|
||||
ShortUrl\Helper\ShortUrlStringifier::class => ['config.url_shortener.domain', 'config.router.base_path'],
|
||||
ShortUrl\Helper\ShortUrlTitleResolutionHelper::class => [Util\UrlValidator::class],
|
||||
ShortUrl\Transformer\ShortUrlDataTransformer::class => [ShortUrl\Helper\ShortUrlStringifier::class],
|
||||
|
||||
Mercure\MercureUpdatesGenerator::class => [ShortUrl\Transformer\ShortUrlDataTransformer::class],
|
||||
|
|
|
@ -84,4 +84,15 @@ return static function (ClassMetadata $metadata, array $emConfig): void {
|
|||
->build();
|
||||
|
||||
$builder->addUniqueConstraint(['short_code', 'domain_id'], 'unique_short_code_plus_domain');
|
||||
|
||||
$builder->createField('title', Types::STRING)
|
||||
->columnName('title')
|
||||
->length(512)
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createField('titleWasAutoResolved', Types::BOOLEAN)
|
||||
->columnName('title_was_auto_resolved')
|
||||
->option('default', false)
|
||||
->build();
|
||||
};
|
||||
|
|
|
@ -26,6 +26,7 @@ const DEFAULT_REDIRECT_STATUS_CODE = StatusCodeInterface::STATUS_FOUND;
|
|||
const DEFAULT_REDIRECT_CACHE_LIFETIME = 30;
|
||||
const LOCAL_LOCK_FACTORY = 'Shlinkio\Shlink\LocalLockFactory';
|
||||
const CUSTOM_SLUGS_REGEXP = '/[^\pL\pN._~]/u'; // Any unicode letter or number, plus ".", "_" and "~" chars
|
||||
const TITLE_TAG_VALUE = '/<title[^>]*>(.*?)<\/title>/i'; // Matches the value inside an html title tag
|
||||
|
||||
function generateRandomShortCode(int $length): string
|
||||
{
|
||||
|
|
|
@ -38,6 +38,8 @@ class ShortUrl extends AbstractEntity
|
|||
private ?string $importSource = null;
|
||||
private ?string $importOriginalShortCode = null;
|
||||
private ?ApiKey $authorApiKey = null;
|
||||
private ?string $title = null;
|
||||
private bool $titleWasAutoResolved = false;
|
||||
|
||||
private function __construct()
|
||||
{
|
||||
|
@ -72,6 +74,8 @@ class ShortUrl extends AbstractEntity
|
|||
$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();
|
||||
|
||||
return $instance;
|
||||
}
|
||||
|
@ -85,6 +89,7 @@ class ShortUrl extends AbstractEntity
|
|||
ShortUrlInputFilter::LONG_URL => $url->longUrl(),
|
||||
ShortUrlInputFilter::DOMAIN => $url->domain(),
|
||||
ShortUrlInputFilter::TAGS => $url->tags(),
|
||||
ShortUrlInputFilter::TITLE => $url->title(),
|
||||
ShortUrlInputFilter::VALIDATE_URL => false,
|
||||
];
|
||||
if ($importShortCode) {
|
||||
|
@ -157,26 +162,39 @@ class ShortUrl extends AbstractEntity
|
|||
return $this->maxVisits;
|
||||
}
|
||||
|
||||
public function getTitle(): ?string
|
||||
{
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
public function update(
|
||||
ShortUrlEdit $shortUrlEdit,
|
||||
?ShortUrlRelationResolverInterface $relationResolver = null
|
||||
): void {
|
||||
if ($shortUrlEdit->hasValidSince()) {
|
||||
if ($shortUrlEdit->validSinceWasProvided()) {
|
||||
$this->validSince = $shortUrlEdit->validSince();
|
||||
}
|
||||
if ($shortUrlEdit->hasValidUntil()) {
|
||||
if ($shortUrlEdit->validUntilWasProvided()) {
|
||||
$this->validUntil = $shortUrlEdit->validUntil();
|
||||
}
|
||||
if ($shortUrlEdit->hasMaxVisits()) {
|
||||
if ($shortUrlEdit->maxVisitsWasProvided()) {
|
||||
$this->maxVisits = $shortUrlEdit->maxVisits();
|
||||
}
|
||||
if ($shortUrlEdit->hasLongUrl()) {
|
||||
$this->longUrl = $shortUrlEdit->longUrl();
|
||||
if ($shortUrlEdit->longUrlWasProvided()) {
|
||||
$this->longUrl = $shortUrlEdit->longUrl() ?? $this->longUrl;
|
||||
}
|
||||
if ($shortUrlEdit->hasTags()) {
|
||||
if ($shortUrlEdit->tagsWereProvided()) {
|
||||
$relationResolver = $relationResolver ?? new SimpleShortUrlRelationResolver();
|
||||
$this->tags = $relationResolver->resolveTags($shortUrlEdit->tags());
|
||||
}
|
||||
if (
|
||||
$this->title === null
|
||||
|| $shortUrlEdit->titleWasProvided()
|
||||
|| ($this->titleWasAutoResolved && $shortUrlEdit->titleWasAutoResolved())
|
||||
) {
|
||||
$this->title = $shortUrlEdit->title();
|
||||
$this->titleWasAutoResolved = $shortUrlEdit->titleWasAutoResolved();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -6,6 +6,7 @@ namespace Shlinkio\Shlink\Core\Model;
|
|||
|
||||
use Cake\Chronos\Chronos;
|
||||
use Shlinkio\Shlink\Core\Exception\ValidationException;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Helper\TitleResolutionModelInterface;
|
||||
use Shlinkio\Shlink\Core\Validation\ShortUrlInputFilter;
|
||||
|
||||
use function array_key_exists;
|
||||
|
@ -13,7 +14,7 @@ use function Shlinkio\Shlink\Core\getOptionalBoolFromInputFilter;
|
|||
use function Shlinkio\Shlink\Core\getOptionalIntFromInputFilter;
|
||||
use function Shlinkio\Shlink\Core\parseDateField;
|
||||
|
||||
final class ShortUrlEdit
|
||||
final class ShortUrlEdit implements TitleResolutionModelInterface
|
||||
{
|
||||
private bool $longUrlPropWasProvided = false;
|
||||
private ?string $longUrl = null;
|
||||
|
@ -25,6 +26,9 @@ final class ShortUrlEdit
|
|||
private ?int $maxVisits = null;
|
||||
private bool $tagsPropWasProvided = false;
|
||||
private array $tags = [];
|
||||
private bool $titlePropWasProvided = false;
|
||||
private ?string $title = null;
|
||||
private bool $titleWasAutoResolved = false;
|
||||
private ?bool $validateUrl = null;
|
||||
|
||||
private function __construct()
|
||||
|
@ -56,6 +60,7 @@ final class ShortUrlEdit
|
|||
$this->validUntilPropWasProvided = array_key_exists(ShortUrlInputFilter::VALID_UNTIL, $data);
|
||||
$this->maxVisitsPropWasProvided = array_key_exists(ShortUrlInputFilter::MAX_VISITS, $data);
|
||||
$this->tagsPropWasProvided = array_key_exists(ShortUrlInputFilter::TAGS, $data);
|
||||
$this->titlePropWasProvided = array_key_exists(ShortUrlInputFilter::TITLE, $data);
|
||||
|
||||
$this->longUrl = $inputFilter->getValue(ShortUrlInputFilter::LONG_URL);
|
||||
$this->validSince = parseDateField($inputFilter->getValue(ShortUrlInputFilter::VALID_SINCE));
|
||||
|
@ -63,6 +68,7 @@ final class ShortUrlEdit
|
|||
$this->maxVisits = getOptionalIntFromInputFilter($inputFilter, ShortUrlInputFilter::MAX_VISITS);
|
||||
$this->validateUrl = getOptionalBoolFromInputFilter($inputFilter, ShortUrlInputFilter::VALIDATE_URL);
|
||||
$this->tags = $inputFilter->getValue(ShortUrlInputFilter::TAGS);
|
||||
$this->title = $inputFilter->getValue(ShortUrlInputFilter::TITLE);
|
||||
}
|
||||
|
||||
public function longUrl(): ?string
|
||||
|
@ -70,7 +76,12 @@ final class ShortUrlEdit
|
|||
return $this->longUrl;
|
||||
}
|
||||
|
||||
public function hasLongUrl(): bool
|
||||
public function getLongUrl(): string
|
||||
{
|
||||
return $this->longUrl() ?? '';
|
||||
}
|
||||
|
||||
public function longUrlWasProvided(): bool
|
||||
{
|
||||
return $this->longUrlPropWasProvided && $this->longUrl !== null;
|
||||
}
|
||||
|
@ -80,7 +91,7 @@ final class ShortUrlEdit
|
|||
return $this->validSince;
|
||||
}
|
||||
|
||||
public function hasValidSince(): bool
|
||||
public function validSinceWasProvided(): bool
|
||||
{
|
||||
return $this->validSincePropWasProvided;
|
||||
}
|
||||
|
@ -90,7 +101,7 @@ final class ShortUrlEdit
|
|||
return $this->validUntil;
|
||||
}
|
||||
|
||||
public function hasValidUntil(): bool
|
||||
public function validUntilWasProvided(): bool
|
||||
{
|
||||
return $this->validUntilPropWasProvided;
|
||||
}
|
||||
|
@ -100,7 +111,7 @@ final class ShortUrlEdit
|
|||
return $this->maxVisits;
|
||||
}
|
||||
|
||||
public function hasMaxVisits(): bool
|
||||
public function maxVisitsWasProvided(): bool
|
||||
{
|
||||
return $this->maxVisitsPropWasProvided;
|
||||
}
|
||||
|
@ -113,11 +124,40 @@ final class ShortUrlEdit
|
|||
return $this->tags;
|
||||
}
|
||||
|
||||
public function hasTags(): bool
|
||||
public function tagsWereProvided(): bool
|
||||
{
|
||||
return $this->tagsPropWasProvided;
|
||||
}
|
||||
|
||||
public function title(): ?string
|
||||
{
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
public function titleWasProvided(): bool
|
||||
{
|
||||
return $this->titlePropWasProvided;
|
||||
}
|
||||
|
||||
public function hasTitle(): bool
|
||||
{
|
||||
return $this->titleWasProvided();
|
||||
}
|
||||
|
||||
public function titleWasAutoResolved(): bool
|
||||
{
|
||||
return $this->titleWasAutoResolved;
|
||||
}
|
||||
|
||||
public function withResolvedTitle(string $title): self
|
||||
{
|
||||
$copy = clone $this;
|
||||
$copy->title = $title;
|
||||
$copy->titleWasAutoResolved = true;
|
||||
|
||||
return $copy;
|
||||
}
|
||||
|
||||
public function doValidateUrl(): ?bool
|
||||
{
|
||||
return $this->validateUrl;
|
||||
|
|
|
@ -6,6 +6,7 @@ namespace Shlinkio\Shlink\Core\Model;
|
|||
|
||||
use Cake\Chronos\Chronos;
|
||||
use Shlinkio\Shlink\Core\Exception\ValidationException;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Helper\TitleResolutionModelInterface;
|
||||
use Shlinkio\Shlink\Core\Validation\ShortUrlInputFilter;
|
||||
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||
|
||||
|
@ -15,7 +16,7 @@ use function Shlinkio\Shlink\Core\parseDateField;
|
|||
|
||||
use const Shlinkio\Shlink\Core\DEFAULT_SHORT_CODES_LENGTH;
|
||||
|
||||
final class ShortUrlMeta
|
||||
final class ShortUrlMeta implements TitleResolutionModelInterface
|
||||
{
|
||||
private string $longUrl;
|
||||
private ?Chronos $validSince = null;
|
||||
|
@ -28,6 +29,8 @@ final class ShortUrlMeta
|
|||
private ?bool $validateUrl = null;
|
||||
private ?ApiKey $apiKey = null;
|
||||
private array $tags = [];
|
||||
private ?string $title = null;
|
||||
private bool $titleWasAutoResolved = false;
|
||||
|
||||
private function __construct()
|
||||
{
|
||||
|
@ -76,6 +79,7 @@ final class ShortUrlMeta
|
|||
) ?? DEFAULT_SHORT_CODES_LENGTH;
|
||||
$this->apiKey = $inputFilter->getValue(ShortUrlInputFilter::API_KEY);
|
||||
$this->tags = $inputFilter->getValue(ShortUrlInputFilter::TAGS);
|
||||
$this->title = $inputFilter->getValue(ShortUrlInputFilter::TITLE);
|
||||
}
|
||||
|
||||
public function getLongUrl(): string
|
||||
|
@ -160,4 +164,28 @@ final class ShortUrlMeta
|
|||
{
|
||||
return $this->tags;
|
||||
}
|
||||
|
||||
public function getTitle(): ?string
|
||||
{
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
public function hasTitle(): bool
|
||||
{
|
||||
return $this->title !== null;
|
||||
}
|
||||
|
||||
public function titleWasAutoResolved(): bool
|
||||
{
|
||||
return $this->titleWasAutoResolved;
|
||||
}
|
||||
|
||||
public function withResolvedTitle(string $title): self
|
||||
{
|
||||
$copy = clone $this;
|
||||
$copy->title = $title;
|
||||
$copy->titleWasAutoResolved = true;
|
||||
|
||||
return $copy;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ namespace Shlinkio\Shlink\Core\Model;
|
|||
|
||||
use Shlinkio\Shlink\Core\Exception\ValidationException;
|
||||
|
||||
use function array_pad;
|
||||
use function explode;
|
||||
use function is_array;
|
||||
use function is_string;
|
||||
|
@ -50,9 +51,9 @@ final class ShortUrlsOrdering
|
|||
|
||||
/** @var string|array $orderBy */
|
||||
if (! $isArray) {
|
||||
$parts = explode('-', $orderBy);
|
||||
$this->orderField = $parts[0];
|
||||
$this->orderDirection = $parts[1] ?? self::DEFAULT_ORDER_DIRECTION;
|
||||
[$field, $dir] = array_pad(explode('-', $orderBy), 2, null);
|
||||
$this->orderField = $field;
|
||||
$this->orderDirection = $dir ?? self::DEFAULT_ORDER_DIRECTION;
|
||||
} else {
|
||||
$this->orderField = key($orderBy);
|
||||
$this->orderDirection = $orderBy[$this->orderField];
|
||||
|
|
|
@ -18,6 +18,7 @@ class UrlShortenerOptions extends AbstractOptions
|
|||
private bool $validateUrl = true;
|
||||
private int $redirectStatusCode = DEFAULT_REDIRECT_STATUS_CODE;
|
||||
private int $redirectCacheLifetime = DEFAULT_REDIRECT_CACHE_LIFETIME;
|
||||
private bool $autoResolveTitles = false;
|
||||
|
||||
public function isUrlValidationEnabled(): bool
|
||||
{
|
||||
|
@ -55,4 +56,15 @@ class UrlShortenerOptions extends AbstractOptions
|
|||
? $redirectCacheLifetime
|
||||
: DEFAULT_REDIRECT_CACHE_LIFETIME;
|
||||
}
|
||||
|
||||
public function autoResolveTitles(): bool
|
||||
{
|
||||
return $this->autoResolveTitles;
|
||||
}
|
||||
|
||||
protected function setAutoResolveTitles(bool $autoResolveTitles): self
|
||||
{
|
||||
$this->autoResolveTitles = $autoResolveTitles;
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -55,6 +55,7 @@ class ShortUrlRepository extends EntitySpecificationRepository implements ShortU
|
|||
$fieldName = $orderBy->orderField();
|
||||
$order = $orderBy->orderDirection();
|
||||
|
||||
// visitsCount and visitCount are deprecated. Only visits should work
|
||||
if (contains(['visits', 'visitsCount', 'visitCount'], $fieldName)) {
|
||||
$qb->addSelect('COUNT(DISTINCT v) AS totalVisits')
|
||||
->leftJoin('s.visits', 'v')
|
||||
|
@ -66,10 +67,11 @@ class ShortUrlRepository extends EntitySpecificationRepository implements ShortU
|
|||
|
||||
// Map public field names to column names
|
||||
$fieldNameMap = [
|
||||
'originalUrl' => 'longUrl',
|
||||
'originalUrl' => 'longUrl', // Deprecated
|
||||
'longUrl' => 'longUrl',
|
||||
'shortCode' => 'shortCode',
|
||||
'dateCreated' => 'dateCreated',
|
||||
'title' => 'title',
|
||||
];
|
||||
if (array_key_exists($fieldName, $fieldNameMap)) {
|
||||
$qb->orderBy('s.' . $fieldNameMap[$fieldName], $order);
|
||||
|
@ -120,6 +122,7 @@ class ShortUrlRepository extends EntitySpecificationRepository implements ShortU
|
|||
->andWhere($qb->expr()->orX(
|
||||
$qb->expr()->like('s.longUrl', ':searchPattern'),
|
||||
$qb->expr()->like('s.shortCode', ':searchPattern'),
|
||||
$qb->expr()->like('s.title', ':searchPattern'),
|
||||
$qb->expr()->like('t.name', ':searchPattern'),
|
||||
$qb->expr()->like('d.authority', ':searchPattern'),
|
||||
))
|
||||
|
|
|
@ -15,26 +15,26 @@ use Shlinkio\Shlink\Core\Model\ShortUrlsParams;
|
|||
use Shlinkio\Shlink\Core\Paginator\Adapter\ShortUrlRepositoryAdapter;
|
||||
use Shlinkio\Shlink\Core\Repository\ShortUrlRepository;
|
||||
use Shlinkio\Shlink\Core\Service\ShortUrl\ShortUrlResolverInterface;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlTitleResolutionHelperInterface;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Resolver\ShortUrlRelationResolverInterface;
|
||||
use Shlinkio\Shlink\Core\Util\UrlValidatorInterface;
|
||||
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||
|
||||
class ShortUrlService implements ShortUrlServiceInterface
|
||||
{
|
||||
private ORM\EntityManagerInterface $em;
|
||||
private ShortUrlResolverInterface $urlResolver;
|
||||
private UrlValidatorInterface $urlValidator;
|
||||
private ShortUrlTitleResolutionHelperInterface $titleResolutionHelper;
|
||||
private ShortUrlRelationResolverInterface $relationResolver;
|
||||
|
||||
public function __construct(
|
||||
ORM\EntityManagerInterface $em,
|
||||
ShortUrlResolverInterface $urlResolver,
|
||||
UrlValidatorInterface $urlValidator,
|
||||
ShortUrlTitleResolutionHelperInterface $titleResolutionHelper,
|
||||
ShortUrlRelationResolverInterface $relationResolver
|
||||
) {
|
||||
$this->em = $em;
|
||||
$this->urlResolver = $urlResolver;
|
||||
$this->urlValidator = $urlValidator;
|
||||
$this->titleResolutionHelper = $titleResolutionHelper;
|
||||
$this->relationResolver = $relationResolver;
|
||||
}
|
||||
|
||||
|
@ -61,8 +61,9 @@ class ShortUrlService implements ShortUrlServiceInterface
|
|||
ShortUrlEdit $shortUrlEdit,
|
||||
?ApiKey $apiKey = null
|
||||
): ShortUrl {
|
||||
if ($shortUrlEdit->hasLongUrl()) {
|
||||
$this->urlValidator->validateUrl($shortUrlEdit->longUrl(), $shortUrlEdit->doValidateUrl());
|
||||
if ($shortUrlEdit->longUrlWasProvided()) {
|
||||
/** @var ShortUrlEdit $shortUrlEdit */
|
||||
$shortUrlEdit = $this->titleResolutionHelper->processTitleAndValidateUrl($shortUrlEdit);
|
||||
}
|
||||
|
||||
$shortUrl = $this->urlResolver->resolveShortUrl($identifier, $apiKey);
|
||||
|
|
|
@ -11,24 +11,23 @@ use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException;
|
|||
use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
|
||||
use Shlinkio\Shlink\Core\Repository\ShortUrlRepositoryInterface;
|
||||
use Shlinkio\Shlink\Core\Service\ShortUrl\ShortCodeHelperInterface;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlTitleResolutionHelperInterface;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Resolver\ShortUrlRelationResolverInterface;
|
||||
use Shlinkio\Shlink\Core\Util\UrlValidatorInterface;
|
||||
use Throwable;
|
||||
|
||||
class UrlShortener implements UrlShortenerInterface
|
||||
{
|
||||
private EntityManagerInterface $em;
|
||||
private UrlValidatorInterface $urlValidator;
|
||||
private ShortUrlTitleResolutionHelperInterface $titleResolutionHelper;
|
||||
private ShortUrlRelationResolverInterface $relationResolver;
|
||||
private ShortCodeHelperInterface $shortCodeHelper;
|
||||
|
||||
public function __construct(
|
||||
UrlValidatorInterface $urlValidator,
|
||||
ShortUrlTitleResolutionHelperInterface $titleResolutionHelper,
|
||||
EntityManagerInterface $em,
|
||||
ShortUrlRelationResolverInterface $relationResolver,
|
||||
ShortCodeHelperInterface $shortCodeHelper
|
||||
) {
|
||||
$this->urlValidator = $urlValidator;
|
||||
$this->titleResolutionHelper = $titleResolutionHelper;
|
||||
$this->em = $em;
|
||||
$this->relationResolver = $relationResolver;
|
||||
$this->shortCodeHelper = $shortCodeHelper;
|
||||
|
@ -37,7 +36,6 @@ class UrlShortener implements UrlShortenerInterface
|
|||
/**
|
||||
* @throws NonUniqueSlugException
|
||||
* @throws InvalidUrlException
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function shorten(ShortUrlMeta $meta): ShortUrl
|
||||
{
|
||||
|
@ -47,7 +45,8 @@ class UrlShortener implements UrlShortenerInterface
|
|||
return $existingShortUrl;
|
||||
}
|
||||
|
||||
$this->urlValidator->validateUrl($meta->getLongUrl(), $meta->doValidateUrl());
|
||||
/** @var ShortUrlMeta $meta */
|
||||
$meta = $this->titleResolutionHelper->processTitleAndValidateUrl($meta);
|
||||
|
||||
return $this->em->transactional(function () use ($meta) {
|
||||
$shortUrl = ShortUrl::fromMeta($meta, $this->relationResolver);
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core\ShortUrl\Helper;
|
||||
|
||||
use Shlinkio\Shlink\Core\Util\UrlValidatorInterface;
|
||||
|
||||
class ShortUrlTitleResolutionHelper implements ShortUrlTitleResolutionHelperInterface
|
||||
{
|
||||
private UrlValidatorInterface $urlValidator;
|
||||
|
||||
public function __construct(UrlValidatorInterface $urlValidator)
|
||||
{
|
||||
$this->urlValidator = $urlValidator;
|
||||
}
|
||||
|
||||
public function processTitleAndValidateUrl(TitleResolutionModelInterface $data): TitleResolutionModelInterface
|
||||
{
|
||||
if ($data->hasTitle()) {
|
||||
$this->urlValidator->validateUrl($data->getLongUrl(), $data->doValidateUrl());
|
||||
return $data;
|
||||
}
|
||||
|
||||
$title = $this->urlValidator->validateUrlWithTitle($data->getLongUrl(), $data->doValidateUrl());
|
||||
return $title === null ? $data : $data->withResolvedTitle($title);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core\ShortUrl\Helper;
|
||||
|
||||
use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
|
||||
|
||||
interface ShortUrlTitleResolutionHelperInterface
|
||||
{
|
||||
/**
|
||||
* @throws InvalidUrlException
|
||||
*/
|
||||
public function processTitleAndValidateUrl(TitleResolutionModelInterface $data): TitleResolutionModelInterface;
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core\ShortUrl\Helper;
|
||||
|
||||
interface TitleResolutionModelInterface
|
||||
{
|
||||
public function hasTitle(): bool;
|
||||
|
||||
public function getLongUrl(): string;
|
||||
|
||||
public function doValidateUrl(): ?bool;
|
||||
|
||||
public function withResolvedTitle(string $title): self;
|
||||
}
|
|
@ -34,6 +34,7 @@ class ShortUrlDataTransformer implements DataTransformerInterface
|
|||
'tags' => invoke($shortUrl->getTags(), '__toString'),
|
||||
'meta' => $this->buildMeta($shortUrl),
|
||||
'domain' => $shortUrl->getDomain(),
|
||||
'title' => $shortUrl->getTitle(),
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
@ -8,9 +8,15 @@ use Fig\Http\Message\RequestMethodInterface;
|
|||
use GuzzleHttp\ClientInterface;
|
||||
use GuzzleHttp\Exception\GuzzleException;
|
||||
use GuzzleHttp\RequestOptions;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
|
||||
use Shlinkio\Shlink\Core\Options\UrlShortenerOptions;
|
||||
|
||||
use function preg_match;
|
||||
use function trim;
|
||||
|
||||
use const Shlinkio\Shlink\Core\TITLE_TAG_VALUE;
|
||||
|
||||
class UrlValidator implements UrlValidatorInterface, RequestMethodInterface
|
||||
{
|
||||
private const MAX_REDIRECTS = 15;
|
||||
|
@ -35,13 +41,39 @@ class UrlValidator implements UrlValidatorInterface, RequestMethodInterface
|
|||
return;
|
||||
}
|
||||
|
||||
$this->validateUrlAndGetResponse($url, true);
|
||||
}
|
||||
|
||||
public function validateUrlWithTitle(string $url, ?bool $doValidate): ?string
|
||||
{
|
||||
$doValidate = $doValidate ?? $this->options->isUrlValidationEnabled();
|
||||
if (! $doValidate && ! $this->options->autoResolveTitles()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$response = $this->validateUrlAndGetResponse($url, $doValidate);
|
||||
if ($response === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$body = $response->getBody()->__toString();
|
||||
preg_match(TITLE_TAG_VALUE, $body, $matches);
|
||||
return isset($matches[1]) ? trim($matches[1]) : null;
|
||||
}
|
||||
|
||||
private function validateUrlAndGetResponse(string $url, bool $throwOnError): ?ResponseInterface
|
||||
{
|
||||
try {
|
||||
$this->httpClient->request(self::METHOD_GET, $url, [
|
||||
return $this->httpClient->request(self::METHOD_GET, $url, [
|
||||
RequestOptions::ALLOW_REDIRECTS => ['max' => self::MAX_REDIRECTS],
|
||||
RequestOptions::IDN_CONVERSION => true,
|
||||
]);
|
||||
} catch (GuzzleException $e) {
|
||||
throw InvalidUrlException::fromUrl($url, $e);
|
||||
if ($throwOnError) {
|
||||
throw InvalidUrlException::fromUrl($url, $e);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,4 +12,9 @@ interface UrlValidatorInterface
|
|||
* @throws InvalidUrlException
|
||||
*/
|
||||
public function validateUrl(string $url, ?bool $doValidate): void;
|
||||
|
||||
/**
|
||||
* @throws InvalidUrlException
|
||||
*/
|
||||
public function validateUrlWithTitle(string $url, ?bool $doValidate): ?string;
|
||||
}
|
||||
|
|
|
@ -31,6 +31,7 @@ class ShortUrlInputFilter extends InputFilter
|
|||
public const VALIDATE_URL = 'validateUrl';
|
||||
public const API_KEY = 'apiKey';
|
||||
public const TAGS = 'tags';
|
||||
public const TITLE = 'title';
|
||||
|
||||
private function __construct(array $data, bool $requireLongUrl)
|
||||
{
|
||||
|
@ -87,6 +88,8 @@ class ShortUrlInputFilter extends InputFilter
|
|||
|
||||
$this->add($this->createBooleanInput(self::FIND_IF_EXISTS, false));
|
||||
|
||||
// This cannot be defined as a boolean input because it can actually have 3 values, true, false and null.
|
||||
// Defining it as boolean will make null fall back to false, which is not the desired behavior.
|
||||
$this->add($this->createInput(self::VALIDATE_URL, false));
|
||||
|
||||
$domain = $this->createInput(self::DOMAIN, false);
|
||||
|
@ -100,5 +103,7 @@ class ShortUrlInputFilter extends InputFilter
|
|||
$this->add($apiKeyInput);
|
||||
|
||||
$this->add($this->createTagsInput(self::TAGS, false));
|
||||
|
||||
$this->add($this->createInput(self::TITLE, false));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -418,7 +418,7 @@ class ShortUrlRepositoryTest extends DatabaseTestCase
|
|||
public function importedShortUrlsAreSearchedAsExpected(): void
|
||||
{
|
||||
$buildImported = static fn (string $shortCode, ?String $domain = null) =>
|
||||
new ImportedShlinkUrl('', 'foo', [], Chronos::now(), $domain, $shortCode);
|
||||
new ImportedShlinkUrl('', 'foo', [], Chronos::now(), $domain, $shortCode, null);
|
||||
|
||||
$shortUrlWithoutDomain = ShortUrl::fromImport($buildImported('my-cool-slug'), true);
|
||||
$this->getEntityManager()->persist($shortUrlWithoutDomain);
|
||||
|
|
|
@ -64,7 +64,7 @@ class ShortUrlTest extends TestCase
|
|||
{
|
||||
yield 'no custom slug' => [ShortUrl::createEmpty()];
|
||||
yield 'imported with custom slug' => [
|
||||
ShortUrl::fromImport(new ImportedShlinkUrl('', '', [], Chronos::now(), null, 'custom-slug'), true),
|
||||
ShortUrl::fromImport(new ImportedShlinkUrl('', '', [], Chronos::now(), null, 'custom-slug', null), true),
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
@ -58,9 +58,9 @@ class ImportedLinksProcessorTest extends TestCase
|
|||
public function newUrlsWithNoErrorsAreAllPersisted(): void
|
||||
{
|
||||
$urls = [
|
||||
new ImportedShlinkUrl('', 'foo', [], Chronos::now(), null, 'foo'),
|
||||
new ImportedShlinkUrl('', 'bar', [], Chronos::now(), null, 'bar'),
|
||||
new ImportedShlinkUrl('', 'baz', [], Chronos::now(), null, 'baz'),
|
||||
new ImportedShlinkUrl('', 'foo', [], Chronos::now(), null, 'foo', null),
|
||||
new ImportedShlinkUrl('', 'bar', [], Chronos::now(), null, 'bar', 'foo'),
|
||||
new ImportedShlinkUrl('', 'baz', [], Chronos::now(), null, 'baz', null),
|
||||
];
|
||||
$expectedCalls = count($urls);
|
||||
|
||||
|
@ -80,11 +80,11 @@ class ImportedLinksProcessorTest extends TestCase
|
|||
public function alreadyImportedUrlsAreSkipped(): void
|
||||
{
|
||||
$urls = [
|
||||
new ImportedShlinkUrl('', 'foo', [], Chronos::now(), null, 'foo'),
|
||||
new ImportedShlinkUrl('', 'bar', [], Chronos::now(), null, 'bar'),
|
||||
new ImportedShlinkUrl('', 'baz', [], Chronos::now(), null, 'baz'),
|
||||
new ImportedShlinkUrl('', 'baz2', [], Chronos::now(), null, 'baz2'),
|
||||
new ImportedShlinkUrl('', 'baz3', [], Chronos::now(), null, 'baz3'),
|
||||
new ImportedShlinkUrl('', 'foo', [], Chronos::now(), null, 'foo', null),
|
||||
new ImportedShlinkUrl('', 'bar', [], Chronos::now(), null, 'bar', null),
|
||||
new ImportedShlinkUrl('', 'baz', [], Chronos::now(), null, 'baz', null),
|
||||
new ImportedShlinkUrl('', 'baz2', [], Chronos::now(), null, 'baz2', null),
|
||||
new ImportedShlinkUrl('', 'baz3', [], Chronos::now(), null, 'baz3', null),
|
||||
];
|
||||
$contains = fn (string $needle) => fn (string $text) => str_contains($text, $needle);
|
||||
|
||||
|
@ -110,11 +110,11 @@ class ImportedLinksProcessorTest extends TestCase
|
|||
public function nonUniqueShortCodesAreAskedToUser(): void
|
||||
{
|
||||
$urls = [
|
||||
new ImportedShlinkUrl('', 'foo', [], Chronos::now(), null, 'foo'),
|
||||
new ImportedShlinkUrl('', 'bar', [], Chronos::now(), null, 'bar'),
|
||||
new ImportedShlinkUrl('', 'baz', [], Chronos::now(), null, 'baz'),
|
||||
new ImportedShlinkUrl('', 'baz2', [], Chronos::now(), null, 'baz2'),
|
||||
new ImportedShlinkUrl('', 'baz3', [], Chronos::now(), null, 'baz3'),
|
||||
new ImportedShlinkUrl('', 'foo', [], Chronos::now(), null, 'foo', null),
|
||||
new ImportedShlinkUrl('', 'bar', [], Chronos::now(), null, 'bar', null),
|
||||
new ImportedShlinkUrl('', 'baz', [], Chronos::now(), null, 'baz', 'foo'),
|
||||
new ImportedShlinkUrl('', 'baz2', [], Chronos::now(), null, 'baz2', null),
|
||||
new ImportedShlinkUrl('', 'baz3', [], Chronos::now(), null, 'baz3', 'bar'),
|
||||
];
|
||||
$contains = fn (string $needle) => fn (string $text) => str_contains($text, $needle);
|
||||
|
||||
|
|
|
@ -28,9 +28,13 @@ class MercureUpdatesGeneratorTest extends TestCase
|
|||
* @test
|
||||
* @dataProvider provideMethod
|
||||
*/
|
||||
public function visitIsProperlySerializedIntoUpdate(string $method, string $expectedTopic): void
|
||||
public function visitIsProperlySerializedIntoUpdate(string $method, string $expectedTopic, ?string $title): void
|
||||
{
|
||||
$shortUrl = ShortUrl::fromMeta(ShortUrlMeta::fromRawData(['customSlug' => 'foo', 'longUrl' => '']));
|
||||
$shortUrl = ShortUrl::fromMeta(ShortUrlMeta::fromRawData([
|
||||
'customSlug' => 'foo',
|
||||
'longUrl' => '',
|
||||
'title' => $title,
|
||||
]));
|
||||
$visit = new Visit($shortUrl, Visitor::emptyInstance());
|
||||
|
||||
$update = $this->generator->{$method}($visit);
|
||||
|
@ -50,6 +54,7 @@ class MercureUpdatesGeneratorTest extends TestCase
|
|||
'maxVisits' => null,
|
||||
],
|
||||
'domain' => null,
|
||||
'title' => $title,
|
||||
],
|
||||
'visit' => [
|
||||
'referer' => '',
|
||||
|
@ -62,7 +67,7 @@ class MercureUpdatesGeneratorTest extends TestCase
|
|||
|
||||
public function provideMethod(): iterable
|
||||
{
|
||||
yield 'newVisitUpdate' => ['newVisitUpdate', 'https://shlink.io/new-visit'];
|
||||
yield 'newShortUrlVisitUpdate' => ['newShortUrlVisitUpdate', 'https://shlink.io/new-visit/foo'];
|
||||
yield 'newVisitUpdate' => ['newVisitUpdate', 'https://shlink.io/new-visit', 'the cool title'];
|
||||
yield 'newShortUrlVisitUpdate' => ['newShortUrlVisitUpdate', 'https://shlink.io/new-visit/foo', null];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,8 +17,8 @@ use Shlinkio\Shlink\Core\Model\ShortUrlsParams;
|
|||
use Shlinkio\Shlink\Core\Repository\ShortUrlRepository;
|
||||
use Shlinkio\Shlink\Core\Service\ShortUrl\ShortUrlResolverInterface;
|
||||
use Shlinkio\Shlink\Core\Service\ShortUrlService;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlTitleResolutionHelperInterface;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Resolver\SimpleShortUrlRelationResolver;
|
||||
use Shlinkio\Shlink\Core\Util\UrlValidatorInterface;
|
||||
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||
use ShlinkioTest\Shlink\Core\Util\ApiKeyHelpersTrait;
|
||||
|
||||
|
@ -32,7 +32,7 @@ class ShortUrlServiceTest extends TestCase
|
|||
private ShortUrlService $service;
|
||||
private ObjectProphecy $em;
|
||||
private ObjectProphecy $urlResolver;
|
||||
private ObjectProphecy $urlValidator;
|
||||
private ObjectProphecy $titleResolutionHelper;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
|
@ -41,12 +41,12 @@ class ShortUrlServiceTest extends TestCase
|
|||
$this->em->flush()->willReturn(null);
|
||||
|
||||
$this->urlResolver = $this->prophesize(ShortUrlResolverInterface::class);
|
||||
$this->urlValidator = $this->prophesize(UrlValidatorInterface::class);
|
||||
$this->titleResolutionHelper = $this->prophesize(ShortUrlTitleResolutionHelperInterface::class);
|
||||
|
||||
$this->service = new ShortUrlService(
|
||||
$this->em->reveal(),
|
||||
$this->urlResolver->reveal(),
|
||||
$this->urlValidator->reveal(),
|
||||
$this->titleResolutionHelper->reveal(),
|
||||
new SimpleShortUrlRelationResolver(),
|
||||
);
|
||||
}
|
||||
|
@ -93,6 +93,10 @@ class ShortUrlServiceTest extends TestCase
|
|||
)->willReturn($shortUrl);
|
||||
$flush = $this->em->flush()->willReturn(null);
|
||||
|
||||
$processTitle = $this->titleResolutionHelper->processTitleAndValidateUrl($shortUrlEdit)->willReturn(
|
||||
$shortUrlEdit,
|
||||
);
|
||||
|
||||
$result = $this->service->updateShortUrl(new ShortUrlIdentifier('abc123'), $shortUrlEdit, $apiKey);
|
||||
|
||||
self::assertSame($shortUrl, $result);
|
||||
|
@ -102,10 +106,7 @@ class ShortUrlServiceTest extends TestCase
|
|||
self::assertEquals($shortUrlEdit->longUrl() ?? $originalLongUrl, $shortUrl->getLongUrl());
|
||||
$findShortUrl->shouldHaveBeenCalled();
|
||||
$flush->shouldHaveBeenCalled();
|
||||
$this->urlValidator->validateUrl(
|
||||
$shortUrlEdit->longUrl(),
|
||||
$shortUrlEdit->doValidateUrl(),
|
||||
)->shouldHaveBeenCalledTimes($expectedValidateCalls);
|
||||
$processTitle->shouldHaveBeenCalledTimes($expectedValidateCalls);
|
||||
}
|
||||
|
||||
public function provideShortUrlEdits(): iterable
|
||||
|
|
|
@ -16,8 +16,8 @@ use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
|
|||
use Shlinkio\Shlink\Core\Repository\ShortUrlRepository;
|
||||
use Shlinkio\Shlink\Core\Service\ShortUrl\ShortCodeHelperInterface;
|
||||
use Shlinkio\Shlink\Core\Service\UrlShortener;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlTitleResolutionHelperInterface;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Resolver\SimpleShortUrlRelationResolver;
|
||||
use Shlinkio\Shlink\Core\Util\UrlValidatorInterface;
|
||||
|
||||
class UrlShortenerTest extends TestCase
|
||||
{
|
||||
|
@ -25,16 +25,13 @@ class UrlShortenerTest extends TestCase
|
|||
|
||||
private UrlShortener $urlShortener;
|
||||
private ObjectProphecy $em;
|
||||
private ObjectProphecy $urlValidator;
|
||||
private ObjectProphecy $titleResolutionHelper;
|
||||
private ObjectProphecy $shortCodeHelper;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
$this->urlValidator = $this->prophesize(UrlValidatorInterface::class);
|
||||
$this->urlValidator->validateUrl('http://foobar.com/12345/hello?foo=bar', null)->will(
|
||||
function (): void {
|
||||
},
|
||||
);
|
||||
$this->titleResolutionHelper = $this->prophesize(ShortUrlTitleResolutionHelperInterface::class);
|
||||
$this->titleResolutionHelper->processTitleAndValidateUrl(Argument::cetera())->willReturnArgument();
|
||||
|
||||
$this->em = $this->prophesize(EntityManagerInterface::class);
|
||||
$this->em->persist(Argument::any())->will(function ($arguments): void {
|
||||
|
@ -56,7 +53,7 @@ class UrlShortenerTest extends TestCase
|
|||
$this->shortCodeHelper->ensureShortCodeUniqueness(Argument::cetera())->willReturn(true);
|
||||
|
||||
$this->urlShortener = new UrlShortener(
|
||||
$this->urlValidator->reveal(),
|
||||
$this->titleResolutionHelper->reveal(),
|
||||
$this->em->reveal(),
|
||||
new SimpleShortUrlRelationResolver(),
|
||||
$this->shortCodeHelper->reveal(),
|
||||
|
@ -66,11 +63,12 @@ class UrlShortenerTest extends TestCase
|
|||
/** @test */
|
||||
public function urlIsProperlyShortened(): void
|
||||
{
|
||||
$shortUrl = $this->urlShortener->shorten(
|
||||
ShortUrlMeta::fromRawData(['longUrl' => 'http://foobar.com/12345/hello?foo=bar']),
|
||||
);
|
||||
$longUrl = 'http://foobar.com/12345/hello?foo=bar';
|
||||
$meta = ShortUrlMeta::fromRawData(['longUrl' => $longUrl]);
|
||||
$shortUrl = $this->urlShortener->shorten($meta);
|
||||
|
||||
self::assertEquals('http://foobar.com/12345/hello?foo=bar', $shortUrl->getLongUrl());
|
||||
self::assertEquals($longUrl, $shortUrl->getLongUrl());
|
||||
$this->titleResolutionHelper->processTitleAndValidateUrl($meta)->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
|
||||
/** @test */
|
||||
|
@ -101,7 +99,7 @@ class UrlShortenerTest extends TestCase
|
|||
$findExisting->shouldHaveBeenCalledOnce();
|
||||
$getRepo->shouldHaveBeenCalledOnce();
|
||||
$this->em->persist(Argument::cetera())->shouldNotHaveBeenCalled();
|
||||
$this->urlValidator->validateUrl(Argument::cetera())->shouldNotHaveBeenCalled();
|
||||
$this->titleResolutionHelper->processTitleAndValidateUrl(Argument::cetera())->shouldNotHaveBeenCalled();
|
||||
self::assertSame($expected, $result);
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\Core\ShortUrl\Helper;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlTitleResolutionHelper;
|
||||
use Shlinkio\Shlink\Core\Util\UrlValidatorInterface;
|
||||
|
||||
class ShortUrlTitleResolutionHelperTest extends TestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
|
||||
private ShortUrlTitleResolutionHelper $helper;
|
||||
private ObjectProphecy $urlValidator;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->urlValidator = $this->prophesize(UrlValidatorInterface::class);
|
||||
$this->helper = new ShortUrlTitleResolutionHelper($this->urlValidator->reveal());
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* @dataProvider provideTitles
|
||||
*/
|
||||
public function urlIsProperlyShortened(?string $title, int $validateWithTitleCallsNum, int $validateCallsNum): void
|
||||
{
|
||||
$longUrl = 'http://foobar.com/12345/hello?foo=bar';
|
||||
$this->helper->processTitleAndValidateUrl(
|
||||
ShortUrlMeta::fromRawData(['longUrl' => $longUrl, 'title' => $title]),
|
||||
);
|
||||
|
||||
$this->urlValidator->validateUrlWithTitle($longUrl, null)->shouldHaveBeenCalledTimes(
|
||||
$validateWithTitleCallsNum,
|
||||
);
|
||||
$this->urlValidator->validateUrl($longUrl, null)->shouldHaveBeenCalledTimes($validateCallsNum);
|
||||
}
|
||||
|
||||
public function provideTitles(): iterable
|
||||
{
|
||||
yield 'no title' => [null, 1, 0];
|
||||
yield 'title' => ['link title', 0, 1];
|
||||
}
|
||||
}
|
|
@ -9,6 +9,7 @@ use GuzzleHttp\ClientInterface;
|
|||
use GuzzleHttp\Exception\ClientException;
|
||||
use GuzzleHttp\RequestOptions;
|
||||
use Laminas\Diactoros\Response;
|
||||
use Laminas\Diactoros\Stream;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
|
@ -76,10 +77,60 @@ class UrlValidatorTest extends TestCase
|
|||
$request->shouldNotHaveBeenCalled();
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* @dataProvider provideDisabledCombinations
|
||||
*/
|
||||
public function validateUrlWithTitleReturnsNullWhenRequestFailsAndValidationIsDisabled(
|
||||
?bool $doValidate,
|
||||
bool $validateUrl
|
||||
): void {
|
||||
$request = $this->httpClient->request(Argument::cetera())->willThrow(ClientException::class);
|
||||
$this->options->validateUrl = $validateUrl;
|
||||
$this->options->autoResolveTitles = true;
|
||||
|
||||
$result = $this->urlValidator->validateUrlWithTitle('http://foobar.com/12345/hello?foo=bar', $doValidate);
|
||||
|
||||
self::assertNull($result);
|
||||
$request->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
|
||||
public function provideDisabledCombinations(): iterable
|
||||
{
|
||||
yield 'config is disabled and no runtime option is provided' => [null, false];
|
||||
yield 'config is enabled but runtime option is disabled' => [false, true];
|
||||
yield 'both config and runtime option are disabled' => [false, false];
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function validateUrlWithTitleReturnsNullWhenAutoResolutionIsDisabled(): void
|
||||
{
|
||||
$request = $this->httpClient->request(Argument::cetera())->willReturn($this->respWithTitle());
|
||||
$this->options->autoResolveTitles = false;
|
||||
|
||||
$result = $this->urlValidator->validateUrlWithTitle('http://foobar.com/12345/hello?foo=bar', false);
|
||||
|
||||
self::assertNull($result);
|
||||
$request->shouldNotHaveBeenCalled();
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function validateUrlWithTitleResolvesTitleWhenAutoResolutionIsEnabled(): void
|
||||
{
|
||||
$request = $this->httpClient->request(Argument::cetera())->willReturn($this->respWithTitle());
|
||||
$this->options->autoResolveTitles = true;
|
||||
|
||||
$result = $this->urlValidator->validateUrlWithTitle('http://foobar.com/12345/hello?foo=bar', true);
|
||||
|
||||
self::assertEquals('Resolved title', $result);
|
||||
$request->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
|
||||
private function respWithTitle(): Response
|
||||
{
|
||||
$body = new Stream('php://temp', 'wr');
|
||||
$body->write('<title> Resolved title</title>');
|
||||
|
||||
return new Response($body);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ use function count;
|
|||
|
||||
class ListShortUrlsTest extends ApiTestCase
|
||||
{
|
||||
private const SHORT_URL_SHLINK = [
|
||||
private const SHORT_URL_SHLINK_WITH_TITLE = [
|
||||
'shortCode' => 'abc123',
|
||||
'shortUrl' => 'http://doma.in/abc123',
|
||||
'longUrl' => 'https://shlink.io',
|
||||
|
@ -25,6 +25,7 @@ class ListShortUrlsTest extends ApiTestCase
|
|||
'maxVisits' => null,
|
||||
],
|
||||
'domain' => null,
|
||||
'title' => 'My cool title',
|
||||
];
|
||||
private const SHORT_URL_DOCS = [
|
||||
'shortCode' => 'ghi789',
|
||||
|
@ -39,6 +40,7 @@ class ListShortUrlsTest extends ApiTestCase
|
|||
'maxVisits' => null,
|
||||
],
|
||||
'domain' => null,
|
||||
'title' => null,
|
||||
];
|
||||
private const SHORT_URL_CUSTOM_SLUG_AND_DOMAIN = [
|
||||
'shortCode' => 'custom-with-domain',
|
||||
|
@ -53,6 +55,7 @@ class ListShortUrlsTest extends ApiTestCase
|
|||
'maxVisits' => null,
|
||||
],
|
||||
'domain' => 'some-domain.com',
|
||||
'title' => null,
|
||||
];
|
||||
private const SHORT_URL_META = [
|
||||
'shortCode' => 'def456',
|
||||
|
@ -69,6 +72,7 @@ class ListShortUrlsTest extends ApiTestCase
|
|||
'maxVisits' => null,
|
||||
],
|
||||
'domain' => null,
|
||||
'title' => null,
|
||||
];
|
||||
private const SHORT_URL_CUSTOM_SLUG = [
|
||||
'shortCode' => 'custom',
|
||||
|
@ -83,6 +87,7 @@ class ListShortUrlsTest extends ApiTestCase
|
|||
'maxVisits' => 2,
|
||||
],
|
||||
'domain' => null,
|
||||
'title' => null,
|
||||
];
|
||||
private const SHORT_URL_CUSTOM_DOMAIN = [
|
||||
'shortCode' => 'ghi789',
|
||||
|
@ -99,6 +104,7 @@ class ListShortUrlsTest extends ApiTestCase
|
|||
'maxVisits' => null,
|
||||
],
|
||||
'domain' => 'example.com',
|
||||
'title' => null,
|
||||
];
|
||||
|
||||
/**
|
||||
|
@ -122,7 +128,7 @@ class ListShortUrlsTest extends ApiTestCase
|
|||
public function provideFilteredLists(): iterable
|
||||
{
|
||||
yield [[], [
|
||||
self::SHORT_URL_SHLINK,
|
||||
self::SHORT_URL_SHLINK_WITH_TITLE,
|
||||
self::SHORT_URL_DOCS,
|
||||
self::SHORT_URL_CUSTOM_SLUG_AND_DOMAIN,
|
||||
self::SHORT_URL_META,
|
||||
|
@ -130,7 +136,7 @@ class ListShortUrlsTest extends ApiTestCase
|
|||
self::SHORT_URL_CUSTOM_DOMAIN,
|
||||
], 'valid_api_key'];
|
||||
yield [['orderBy' => 'shortCode'], [
|
||||
self::SHORT_URL_SHLINK,
|
||||
self::SHORT_URL_SHLINK_WITH_TITLE,
|
||||
self::SHORT_URL_CUSTOM_SLUG,
|
||||
self::SHORT_URL_CUSTOM_SLUG_AND_DOMAIN,
|
||||
self::SHORT_URL_META,
|
||||
|
@ -143,7 +149,7 @@ class ListShortUrlsTest extends ApiTestCase
|
|||
self::SHORT_URL_META,
|
||||
self::SHORT_URL_CUSTOM_SLUG_AND_DOMAIN,
|
||||
self::SHORT_URL_CUSTOM_SLUG,
|
||||
self::SHORT_URL_SHLINK,
|
||||
self::SHORT_URL_SHLINK_WITH_TITLE,
|
||||
], 'valid_api_key'];
|
||||
yield [['orderBy' => 'shortCode-DESC'], [
|
||||
self::SHORT_URL_DOCS,
|
||||
|
@ -151,7 +157,15 @@ class ListShortUrlsTest extends ApiTestCase
|
|||
self::SHORT_URL_META,
|
||||
self::SHORT_URL_CUSTOM_SLUG_AND_DOMAIN,
|
||||
self::SHORT_URL_CUSTOM_SLUG,
|
||||
self::SHORT_URL_SHLINK,
|
||||
self::SHORT_URL_SHLINK_WITH_TITLE,
|
||||
], 'valid_api_key'];
|
||||
yield [['orderBy' => 'title-DESC'], [
|
||||
self::SHORT_URL_META,
|
||||
self::SHORT_URL_CUSTOM_SLUG,
|
||||
self::SHORT_URL_DOCS,
|
||||
self::SHORT_URL_CUSTOM_DOMAIN,
|
||||
self::SHORT_URL_CUSTOM_SLUG_AND_DOMAIN,
|
||||
self::SHORT_URL_SHLINK_WITH_TITLE,
|
||||
], 'valid_api_key'];
|
||||
yield [['startDate' => Chronos::parse('2018-12-01')->toAtomString()], [
|
||||
self::SHORT_URL_META,
|
||||
|
@ -159,12 +173,12 @@ class ListShortUrlsTest extends ApiTestCase
|
|||
self::SHORT_URL_CUSTOM_DOMAIN,
|
||||
], 'valid_api_key'];
|
||||
yield [['endDate' => Chronos::parse('2018-12-01')->toAtomString()], [
|
||||
self::SHORT_URL_SHLINK,
|
||||
self::SHORT_URL_SHLINK_WITH_TITLE,
|
||||
self::SHORT_URL_DOCS,
|
||||
self::SHORT_URL_CUSTOM_SLUG_AND_DOMAIN,
|
||||
], 'valid_api_key'];
|
||||
yield [['tags' => ['foo']], [
|
||||
self::SHORT_URL_SHLINK,
|
||||
self::SHORT_URL_SHLINK_WITH_TITLE,
|
||||
self::SHORT_URL_META,
|
||||
self::SHORT_URL_CUSTOM_DOMAIN,
|
||||
], 'valid_api_key'];
|
||||
|
@ -172,17 +186,20 @@ class ListShortUrlsTest extends ApiTestCase
|
|||
self::SHORT_URL_META,
|
||||
], 'valid_api_key'];
|
||||
yield [['tags' => ['foo'], 'endDate' => Chronos::parse('2018-12-01')->toAtomString()], [
|
||||
self::SHORT_URL_SHLINK,
|
||||
self::SHORT_URL_SHLINK_WITH_TITLE,
|
||||
], 'valid_api_key'];
|
||||
yield [['searchTerm' => 'alejandro'], [
|
||||
self::SHORT_URL_META,
|
||||
self::SHORT_URL_CUSTOM_DOMAIN,
|
||||
], 'valid_api_key'];
|
||||
yield [['searchTerm' => 'cool'], [
|
||||
self::SHORT_URL_SHLINK_WITH_TITLE,
|
||||
], 'valid_api_key'];
|
||||
yield [['searchTerm' => 'example.com'], [
|
||||
self::SHORT_URL_CUSTOM_DOMAIN,
|
||||
], 'valid_api_key'];
|
||||
yield [[], [
|
||||
self::SHORT_URL_SHLINK,
|
||||
self::SHORT_URL_SHLINK_WITH_TITLE,
|
||||
self::SHORT_URL_META,
|
||||
self::SHORT_URL_CUSTOM_SLUG,
|
||||
], 'author_api_key'];
|
||||
|
|
|
@ -34,6 +34,7 @@ class ShortUrlsFixture extends AbstractFixture implements DependentFixtureInterf
|
|||
'apiKey' => $authorApiKey,
|
||||
'longUrl' => 'https://shlink.io',
|
||||
'tags' => ['foo'],
|
||||
'title' => 'My cool title',
|
||||
]), $relationResolver),
|
||||
'2018-05-01',
|
||||
);
|
||||
|
|
Loading…
Add table
Reference in a new issue