mirror of
https://github.com/shlinkio/shlink.git
synced 2024-11-23 21:27:44 +03:00
Updated logic to generate random short codes, increasing entropy
This commit is contained in:
parent
c8d950e04d
commit
2f09ff456c
13 changed files with 91 additions and 149 deletions
|
@ -33,6 +33,7 @@
|
||||||
"ocramius/proxy-manager": "~2.2.2",
|
"ocramius/proxy-manager": "~2.2.2",
|
||||||
"phly/phly-event-dispatcher": "^1.0",
|
"phly/phly-event-dispatcher": "^1.0",
|
||||||
"predis/predis": "^1.1",
|
"predis/predis": "^1.1",
|
||||||
|
"pugx/shortid-php": "^0.5",
|
||||||
"shlinkio/shlink-common": "^2.0",
|
"shlinkio/shlink-common": "^2.0",
|
||||||
"shlinkio/shlink-event-dispatcher": "^1.0",
|
"shlinkio/shlink-event-dispatcher": "^1.0",
|
||||||
"shlinkio/shlink-installer": "^2.1",
|
"shlinkio/shlink-installer": "^2.1",
|
||||||
|
|
|
@ -2,8 +2,6 @@
|
||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
use Shlinkio\Shlink\Core\Options\UrlShortenerOptions;
|
|
||||||
|
|
||||||
use function Shlinkio\Shlink\Common\env;
|
use function Shlinkio\Shlink\Common\env;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
@ -13,7 +11,6 @@ return [
|
||||||
'schema' => env('SHORTENED_URL_SCHEMA', 'http'),
|
'schema' => env('SHORTENED_URL_SCHEMA', 'http'),
|
||||||
'hostname' => env('SHORTENED_URL_HOSTNAME'),
|
'hostname' => env('SHORTENED_URL_HOSTNAME'),
|
||||||
],
|
],
|
||||||
'shortcode_chars' => env('SHORTCODE_CHARS', UrlShortenerOptions::DEFAULT_CHARS),
|
|
||||||
'validate_url' => true,
|
'validate_url' => true,
|
||||||
'not_found_short_url' => [
|
'not_found_short_url' => [
|
||||||
'enable_redirection' => false,
|
'enable_redirection' => false,
|
||||||
|
|
|
@ -5,7 +5,6 @@ declare(strict_types=1);
|
||||||
namespace Shlinkio\Shlink\CLI\Command\Config;
|
namespace Shlinkio\Shlink\CLI\Command\Config;
|
||||||
|
|
||||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
||||||
use Shlinkio\Shlink\Core\Options\UrlShortenerOptions;
|
|
||||||
use Symfony\Component\Console\Command\Command;
|
use Symfony\Component\Console\Command\Command;
|
||||||
use Symfony\Component\Console\Input\InputInterface;
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
use Symfony\Component\Console\Output\OutputInterface;
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
@ -18,6 +17,7 @@ use function str_shuffle;
|
||||||
class GenerateCharsetCommand extends Command
|
class GenerateCharsetCommand extends Command
|
||||||
{
|
{
|
||||||
public const NAME = 'config:generate-charset';
|
public const NAME = 'config:generate-charset';
|
||||||
|
private const DEFAULT_CHARS = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||||
|
|
||||||
protected function configure(): void
|
protected function configure(): void
|
||||||
{
|
{
|
||||||
|
@ -26,14 +26,14 @@ class GenerateCharsetCommand extends Command
|
||||||
->setDescription(sprintf(
|
->setDescription(sprintf(
|
||||||
'[DEPRECATED] Generates a character set sample just by shuffling the default one, "%s". '
|
'[DEPRECATED] Generates a character set sample just by shuffling the default one, "%s". '
|
||||||
. 'Then it can be set in the SHORTCODE_CHARS environment variable',
|
. 'Then it can be set in the SHORTCODE_CHARS environment variable',
|
||||||
UrlShortenerOptions::DEFAULT_CHARS
|
self::DEFAULT_CHARS
|
||||||
))
|
))
|
||||||
->setHelp('<fg=red;options=bold>This command is deprecated. Better leave shlink generate the charset.</>');
|
->setHelp('<fg=red;options=bold>This command is deprecated. Better leave shlink generate the charset.</>');
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function execute(InputInterface $input, OutputInterface $output): ?int
|
protected function execute(InputInterface $input, OutputInterface $output): ?int
|
||||||
{
|
{
|
||||||
$charSet = str_shuffle(UrlShortenerOptions::DEFAULT_CHARS);
|
$charSet = str_shuffle(self::DEFAULT_CHARS);
|
||||||
(new SymfonyStyle($input, $output))->success(sprintf('Character set: "%s"', $charSet));
|
(new SymfonyStyle($input, $output))->success(sprintf('Character set: "%s"', $charSet));
|
||||||
return ExitCodes::EXIT_SUCCESS;
|
return ExitCodes::EXIT_SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,11 @@ use Symfony\Component\Console\Tester\CommandTester;
|
||||||
|
|
||||||
class GenerateShortUrlCommandTest extends TestCase
|
class GenerateShortUrlCommandTest extends TestCase
|
||||||
{
|
{
|
||||||
|
private const DOMAIN_CONFIG = [
|
||||||
|
'schema' => 'http',
|
||||||
|
'hostname' => 'foo.com',
|
||||||
|
];
|
||||||
|
|
||||||
/** @var CommandTester */
|
/** @var CommandTester */
|
||||||
private $commandTester;
|
private $commandTester;
|
||||||
/** @var ObjectProphecy */
|
/** @var ObjectProphecy */
|
||||||
|
@ -28,10 +33,7 @@ class GenerateShortUrlCommandTest extends TestCase
|
||||||
public function setUp(): void
|
public function setUp(): void
|
||||||
{
|
{
|
||||||
$this->urlShortener = $this->prophesize(UrlShortener::class);
|
$this->urlShortener = $this->prophesize(UrlShortener::class);
|
||||||
$command = new GenerateShortUrlCommand($this->urlShortener->reveal(), [
|
$command = new GenerateShortUrlCommand($this->urlShortener->reveal(), self::DOMAIN_CONFIG);
|
||||||
'schema' => 'http',
|
|
||||||
'hostname' => 'foo.com',
|
|
||||||
]);
|
|
||||||
$app = new Application();
|
$app = new Application();
|
||||||
$app->add($command);
|
$app->add($command);
|
||||||
$this->commandTester = new CommandTester($command);
|
$this->commandTester = new CommandTester($command);
|
||||||
|
@ -40,9 +42,8 @@ class GenerateShortUrlCommandTest extends TestCase
|
||||||
/** @test */
|
/** @test */
|
||||||
public function properShortCodeIsCreatedIfLongUrlIsCorrect(): void
|
public function properShortCodeIsCreatedIfLongUrlIsCorrect(): void
|
||||||
{
|
{
|
||||||
$urlToShortCode = $this->urlShortener->urlToShortCode(Argument::cetera())->willReturn(
|
$shortUrl = new ShortUrl('');
|
||||||
(new ShortUrl(''))->setShortCode('abc123')
|
$urlToShortCode = $this->urlShortener->urlToShortCode(Argument::cetera())->willReturn($shortUrl);
|
||||||
);
|
|
||||||
|
|
||||||
$this->commandTester->execute([
|
$this->commandTester->execute([
|
||||||
'longUrl' => 'http://domain.com/foo/bar',
|
'longUrl' => 'http://domain.com/foo/bar',
|
||||||
|
@ -51,7 +52,7 @@ class GenerateShortUrlCommandTest extends TestCase
|
||||||
$output = $this->commandTester->getDisplay();
|
$output = $this->commandTester->getDisplay();
|
||||||
|
|
||||||
$this->assertEquals(ExitCodes::EXIT_SUCCESS, $this->commandTester->getStatusCode());
|
$this->assertEquals(ExitCodes::EXIT_SUCCESS, $this->commandTester->getStatusCode());
|
||||||
$this->assertStringContainsString('http://foo.com/abc123', $output);
|
$this->assertStringContainsString($shortUrl->toString(self::DOMAIN_CONFIG), $output);
|
||||||
$urlToShortCode->shouldHaveBeenCalledOnce();
|
$urlToShortCode->shouldHaveBeenCalledOnce();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -86,6 +87,7 @@ class GenerateShortUrlCommandTest extends TestCase
|
||||||
/** @test */
|
/** @test */
|
||||||
public function properlyProcessesProvidedTags(): void
|
public function properlyProcessesProvidedTags(): void
|
||||||
{
|
{
|
||||||
|
$shortUrl = new ShortUrl('');
|
||||||
$urlToShortCode = $this->urlShortener->urlToShortCode(
|
$urlToShortCode = $this->urlShortener->urlToShortCode(
|
||||||
Argument::type(UriInterface::class),
|
Argument::type(UriInterface::class),
|
||||||
Argument::that(function (array $tags) {
|
Argument::that(function (array $tags) {
|
||||||
|
@ -93,7 +95,7 @@ class GenerateShortUrlCommandTest extends TestCase
|
||||||
return $tags;
|
return $tags;
|
||||||
}),
|
}),
|
||||||
Argument::cetera()
|
Argument::cetera()
|
||||||
)->willReturn((new ShortUrl(''))->setShortCode('abc123'));
|
)->willReturn($shortUrl);
|
||||||
|
|
||||||
$this->commandTester->execute([
|
$this->commandTester->execute([
|
||||||
'longUrl' => 'http://domain.com/foo/bar',
|
'longUrl' => 'http://domain.com/foo/bar',
|
||||||
|
@ -102,7 +104,7 @@ class GenerateShortUrlCommandTest extends TestCase
|
||||||
$output = $this->commandTester->getDisplay();
|
$output = $this->commandTester->getDisplay();
|
||||||
|
|
||||||
$this->assertEquals(ExitCodes::EXIT_SUCCESS, $this->commandTester->getStatusCode());
|
$this->assertEquals(ExitCodes::EXIT_SUCCESS, $this->commandTester->getStatusCode());
|
||||||
$this->assertStringContainsString('http://foo.com/abc123', $output);
|
$this->assertStringContainsString($shortUrl->toString(self::DOMAIN_CONFIG), $output);
|
||||||
$urlToShortCode->shouldHaveBeenCalledOnce();
|
$urlToShortCode->shouldHaveBeenCalledOnce();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ namespace Shlinkio\Shlink\Core\Entity;
|
||||||
use Cake\Chronos\Chronos;
|
use Cake\Chronos\Chronos;
|
||||||
use Doctrine\Common\Collections\ArrayCollection;
|
use Doctrine\Common\Collections\ArrayCollection;
|
||||||
use Doctrine\Common\Collections\Collection;
|
use Doctrine\Common\Collections\Collection;
|
||||||
|
use PUGX\Shortid\Factory as ShortIdFactory;
|
||||||
use Shlinkio\Shlink\Common\Entity\AbstractEntity;
|
use Shlinkio\Shlink\Common\Entity\AbstractEntity;
|
||||||
use Shlinkio\Shlink\Core\Domain\Resolver\DomainResolverInterface;
|
use Shlinkio\Shlink\Core\Domain\Resolver\DomainResolverInterface;
|
||||||
use Shlinkio\Shlink\Core\Domain\Resolver\SimpleDomainResolver;
|
use Shlinkio\Shlink\Core\Domain\Resolver\SimpleDomainResolver;
|
||||||
|
@ -20,6 +21,8 @@ use function Functional\invoke;
|
||||||
|
|
||||||
class ShortUrl extends AbstractEntity
|
class ShortUrl extends AbstractEntity
|
||||||
{
|
{
|
||||||
|
private const BASE62 = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||||
|
|
||||||
/** @var string */
|
/** @var string */
|
||||||
private $longUrl;
|
private $longUrl;
|
||||||
/** @var string */
|
/** @var string */
|
||||||
|
@ -53,10 +56,15 @@ class ShortUrl extends AbstractEntity
|
||||||
$this->validSince = $meta->getValidSince();
|
$this->validSince = $meta->getValidSince();
|
||||||
$this->validUntil = $meta->getValidUntil();
|
$this->validUntil = $meta->getValidUntil();
|
||||||
$this->maxVisits = $meta->getMaxVisits();
|
$this->maxVisits = $meta->getMaxVisits();
|
||||||
$this->shortCode = $meta->getCustomSlug() ?? ''; // TODO logic to calculate short code should be passed somehow
|
$this->shortCode = $meta->getCustomSlug() ?? $this->generateShortCode();
|
||||||
$this->domain = ($domainResolver ?? new SimpleDomainResolver())->resolveDomain($meta->getDomain());
|
$this->domain = ($domainResolver ?? new SimpleDomainResolver())->resolveDomain($meta->getDomain());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function generateShortCode(): string
|
||||||
|
{
|
||||||
|
return (new ShortIdFactory())->generate(6, self::BASE62)->serialize();
|
||||||
|
}
|
||||||
|
|
||||||
public function getLongUrl(): string
|
public function getLongUrl(): string
|
||||||
{
|
{
|
||||||
return $this->longUrl;
|
return $this->longUrl;
|
||||||
|
@ -67,13 +75,6 @@ class ShortUrl extends AbstractEntity
|
||||||
return $this->shortCode;
|
return $this->shortCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO Short code is currently calculated based on the ID, so a setter is needed
|
|
||||||
public function setShortCode(string $shortCode): self
|
|
||||||
{
|
|
||||||
$this->shortCode = $shortCode;
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getDateCreated(): Chronos
|
public function getDateCreated(): Chronos
|
||||||
{
|
{
|
||||||
return $this->dateCreated;
|
return $this->dateCreated;
|
||||||
|
|
|
@ -8,26 +8,12 @@ use Zend\Stdlib\AbstractOptions;
|
||||||
|
|
||||||
class UrlShortenerOptions extends AbstractOptions
|
class UrlShortenerOptions extends AbstractOptions
|
||||||
{
|
{
|
||||||
public const DEFAULT_CHARS = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
|
||||||
|
|
||||||
// phpcs:disable
|
// phpcs:disable
|
||||||
protected $__strictMode__ = false;
|
protected $__strictMode__ = false;
|
||||||
// phpcs:enable
|
// phpcs:enable
|
||||||
|
|
||||||
private $shortcodeChars = self::DEFAULT_CHARS;
|
|
||||||
private $validateUrl = true;
|
private $validateUrl = true;
|
||||||
|
|
||||||
public function getChars(): string
|
|
||||||
{
|
|
||||||
return $this->shortcodeChars;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function setShortcodeChars(string $shortcodeChars): self
|
|
||||||
{
|
|
||||||
$this->shortcodeChars = empty($shortcodeChars) ? self::DEFAULT_CHARS : $shortcodeChars;
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function isUrlValidationEnabled(): bool
|
public function isUrlValidationEnabled(): bool
|
||||||
{
|
{
|
||||||
return $this->validateUrl;
|
return $this->validateUrl;
|
||||||
|
|
|
@ -24,17 +24,11 @@ use Shlinkio\Shlink\Core\Util\TagManagerTrait;
|
||||||
use Throwable;
|
use Throwable;
|
||||||
|
|
||||||
use function array_reduce;
|
use function array_reduce;
|
||||||
use function floor;
|
|
||||||
use function fmod;
|
|
||||||
use function preg_match;
|
|
||||||
use function strlen;
|
|
||||||
|
|
||||||
class UrlShortener implements UrlShortenerInterface
|
class UrlShortener implements UrlShortenerInterface
|
||||||
{
|
{
|
||||||
use TagManagerTrait;
|
use TagManagerTrait;
|
||||||
|
|
||||||
private const ID_INCREMENT = 200000;
|
|
||||||
|
|
||||||
/** @var ClientInterface */
|
/** @var ClientInterface */
|
||||||
private $httpClient;
|
private $httpClient;
|
||||||
/** @var EntityManagerInterface */
|
/** @var EntityManagerInterface */
|
||||||
|
@ -77,16 +71,8 @@ class UrlShortener implements UrlShortenerInterface
|
||||||
|
|
||||||
// First, create the short URL with an empty short code
|
// First, create the short URL with an empty short code
|
||||||
$shortUrl = new ShortUrl($url, $meta, new PersistenceDomainResolver($this->em));
|
$shortUrl = new ShortUrl($url, $meta, new PersistenceDomainResolver($this->em));
|
||||||
$this->em->persist($shortUrl);
|
|
||||||
$this->em->flush();
|
|
||||||
|
|
||||||
// Generate the short code and persist it if no custom slug was provided
|
|
||||||
if (! $meta->hasCustomSlug()) {
|
|
||||||
// TODO Somehow provide the logic to calculate the shortCode to avoid the need of a setter
|
|
||||||
$shortCode = $this->convertAutoincrementIdToShortCode((float) $shortUrl->getId());
|
|
||||||
$shortUrl->setShortCode($shortCode);
|
|
||||||
}
|
|
||||||
$shortUrl->setTags($this->tagNamesToEntities($this->em, $tags));
|
$shortUrl->setTags($this->tagNamesToEntities($this->em, $tags));
|
||||||
|
$this->em->persist($shortUrl);
|
||||||
$this->em->flush();
|
$this->em->flush();
|
||||||
|
|
||||||
$this->em->commit();
|
$this->em->commit();
|
||||||
|
@ -155,36 +141,12 @@ class UrlShortener implements UrlShortenerInterface
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function convertAutoincrementIdToShortCode(float $id): string
|
|
||||||
{
|
|
||||||
$id += self::ID_INCREMENT; // Increment the Id so that the generated shortcode is not too short
|
|
||||||
$chars = $this->options->getChars();
|
|
||||||
|
|
||||||
$length = strlen($chars);
|
|
||||||
$code = '';
|
|
||||||
|
|
||||||
while ($id > 0) {
|
|
||||||
// Determine the value of the next higher character in the short code and prepend it
|
|
||||||
$code = $chars[(int) fmod($id, $length)] . $code;
|
|
||||||
$id = floor($id / $length);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $chars[(int) $id] . $code;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @throws InvalidShortCodeException
|
* @throws InvalidShortCodeException
|
||||||
* @throws EntityDoesNotExistException
|
* @throws EntityDoesNotExistException
|
||||||
*/
|
*/
|
||||||
public function shortCodeToUrl(string $shortCode, ?string $domain = null): ShortUrl
|
public function shortCodeToUrl(string $shortCode, ?string $domain = null): ShortUrl
|
||||||
{
|
{
|
||||||
$chars = $this->options->getChars();
|
|
||||||
|
|
||||||
// Validate short code format
|
|
||||||
if (! preg_match('|[' . $chars . ']+|', $shortCode)) {
|
|
||||||
throw InvalidShortCodeException::fromCharset($shortCode, $chars);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @var ShortUrlRepository $shortUrlRepo */
|
/** @var ShortUrlRepository $shortUrlRepo */
|
||||||
$shortUrlRepo = $this->em->getRepository(ShortUrl::class);
|
$shortUrlRepo = $this->em->getRepository(ShortUrl::class);
|
||||||
$shortUrl = $shortUrlRepo->findOneByShortCode($shortCode, $domain);
|
$shortUrl = $shortUrlRepo->findOneByShortCode($shortCode, $domain);
|
||||||
|
|
|
@ -37,37 +37,41 @@ class ShortUrlRepositoryTest extends DatabaseTestCase
|
||||||
/** @test */
|
/** @test */
|
||||||
public function findOneByShortCodeReturnsProperData(): void
|
public function findOneByShortCodeReturnsProperData(): void
|
||||||
{
|
{
|
||||||
$regularOne = new ShortUrl('foo');
|
$regularOne = new ShortUrl('foo', ShortUrlMeta::createFromParams(null, null, 'foo'));
|
||||||
$regularOne->setShortCode('foo');
|
|
||||||
$this->getEntityManager()->persist($regularOne);
|
$this->getEntityManager()->persist($regularOne);
|
||||||
|
|
||||||
$notYetValid = new ShortUrl('bar', ShortUrlMeta::createFromParams(Chronos::now()->addMonth()));
|
$notYetValid = new ShortUrl(
|
||||||
$notYetValid->setShortCode('bar_very_long_text');
|
'bar',
|
||||||
|
ShortUrlMeta::createFromParams(Chronos::now()->addMonth(), null, 'bar_very_long_text')
|
||||||
|
);
|
||||||
$this->getEntityManager()->persist($notYetValid);
|
$this->getEntityManager()->persist($notYetValid);
|
||||||
|
|
||||||
$expired = new ShortUrl('expired', ShortUrlMeta::createFromParams(null, Chronos::now()->subMonth()));
|
$expired = new ShortUrl('expired', ShortUrlMeta::createFromParams(null, Chronos::now()->subMonth(), 'expired'));
|
||||||
$expired->setShortCode('expired');
|
|
||||||
$this->getEntityManager()->persist($expired);
|
$this->getEntityManager()->persist($expired);
|
||||||
|
|
||||||
$allVisitsComplete = new ShortUrl('baz', ShortUrlMeta::createFromRawData(['maxVisits' => 3]));
|
$allVisitsComplete = new ShortUrl('baz', ShortUrlMeta::createFromRawData([
|
||||||
|
'maxVisits' => 3,
|
||||||
|
'customSlug' => 'baz',
|
||||||
|
]));
|
||||||
$visits = [];
|
$visits = [];
|
||||||
for ($i = 0; $i < 3; $i++) {
|
for ($i = 0; $i < 3; $i++) {
|
||||||
$visit = new Visit($allVisitsComplete, Visitor::emptyInstance());
|
$visit = new Visit($allVisitsComplete, Visitor::emptyInstance());
|
||||||
$this->getEntityManager()->persist($visit);
|
$this->getEntityManager()->persist($visit);
|
||||||
$visits[] = $visit;
|
$visits[] = $visit;
|
||||||
}
|
}
|
||||||
$allVisitsComplete->setShortCode('baz')
|
$allVisitsComplete->setVisits(new ArrayCollection($visits));
|
||||||
->setVisits(new ArrayCollection($visits));
|
|
||||||
$this->getEntityManager()->persist($allVisitsComplete);
|
$this->getEntityManager()->persist($allVisitsComplete);
|
||||||
|
|
||||||
$withDomain = new ShortUrl('foo', ShortUrlMeta::createFromRawData(['domain' => 'example.com']));
|
$withDomain = new ShortUrl('foo', ShortUrlMeta::createFromRawData([
|
||||||
$withDomain->setShortCode('domain-short-code');
|
'domain' => 'example.com',
|
||||||
|
'customSlug' => 'domain-short-code',
|
||||||
|
]));
|
||||||
$this->getEntityManager()->persist($withDomain);
|
$this->getEntityManager()->persist($withDomain);
|
||||||
|
|
||||||
$withDomainDuplicatingRegular = new ShortUrl('foo_with_domain', ShortUrlMeta::createFromRawData([
|
$withDomainDuplicatingRegular = new ShortUrl('foo_with_domain', ShortUrlMeta::createFromRawData([
|
||||||
'domain' => 'doma.in',
|
'domain' => 'doma.in',
|
||||||
|
'customSlug' => 'foo',
|
||||||
]));
|
]));
|
||||||
$withDomainDuplicatingRegular->setShortCode('foo');
|
|
||||||
$this->getEntityManager()->persist($withDomainDuplicatingRegular);
|
$this->getEntityManager()->persist($withDomainDuplicatingRegular);
|
||||||
|
|
||||||
$this->getEntityManager()->flush();
|
$this->getEntityManager()->flush();
|
||||||
|
@ -96,9 +100,7 @@ class ShortUrlRepositoryTest extends DatabaseTestCase
|
||||||
{
|
{
|
||||||
$count = 5;
|
$count = 5;
|
||||||
for ($i = 0; $i < $count; $i++) {
|
for ($i = 0; $i < $count; $i++) {
|
||||||
$this->getEntityManager()->persist(
|
$this->getEntityManager()->persist(new ShortUrl((string) $i));
|
||||||
(new ShortUrl((string) $i))->setShortCode((string) $i)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
$this->getEntityManager()->flush();
|
$this->getEntityManager()->flush();
|
||||||
|
|
||||||
|
@ -112,19 +114,16 @@ class ShortUrlRepositoryTest extends DatabaseTestCase
|
||||||
$this->getEntityManager()->persist($tag);
|
$this->getEntityManager()->persist($tag);
|
||||||
|
|
||||||
$foo = new ShortUrl('foo');
|
$foo = new ShortUrl('foo');
|
||||||
$foo->setShortCode('foo')
|
$foo->setTags(new ArrayCollection([$tag]));
|
||||||
->setTags(new ArrayCollection([$tag]));
|
|
||||||
$this->getEntityManager()->persist($foo);
|
$this->getEntityManager()->persist($foo);
|
||||||
|
|
||||||
$bar = new ShortUrl('bar');
|
$bar = new ShortUrl('bar');
|
||||||
$visit = new Visit($bar, Visitor::emptyInstance());
|
$visit = new Visit($bar, Visitor::emptyInstance());
|
||||||
$this->getEntityManager()->persist($visit);
|
$this->getEntityManager()->persist($visit);
|
||||||
$bar->setShortCode('bar_very_long_text')
|
$bar->setVisits(new ArrayCollection([$visit]));
|
||||||
->setVisits(new ArrayCollection([$visit]));
|
|
||||||
$this->getEntityManager()->persist($bar);
|
$this->getEntityManager()->persist($bar);
|
||||||
|
|
||||||
$foo2 = new ShortUrl('foo_2');
|
$foo2 = new ShortUrl('foo_2');
|
||||||
$foo2->setShortCode('foo_2');
|
|
||||||
$this->getEntityManager()->persist($foo2);
|
$this->getEntityManager()->persist($foo2);
|
||||||
|
|
||||||
$this->getEntityManager()->flush();
|
$this->getEntityManager()->flush();
|
||||||
|
@ -155,9 +154,7 @@ class ShortUrlRepositoryTest extends DatabaseTestCase
|
||||||
{
|
{
|
||||||
$urls = ['a', 'z', 'c', 'b'];
|
$urls = ['a', 'z', 'c', 'b'];
|
||||||
foreach ($urls as $url) {
|
foreach ($urls as $url) {
|
||||||
$this->getEntityManager()->persist(
|
$this->getEntityManager()->persist(new ShortUrl($url));
|
||||||
(new ShortUrl($url))->setShortCode($url)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->getEntityManager()->flush();
|
$this->getEntityManager()->flush();
|
||||||
|
@ -174,13 +171,13 @@ class ShortUrlRepositoryTest extends DatabaseTestCase
|
||||||
/** @test */
|
/** @test */
|
||||||
public function slugIsInUseLooksForShortUrlInProperSetOfTables(): void
|
public function slugIsInUseLooksForShortUrlInProperSetOfTables(): void
|
||||||
{
|
{
|
||||||
$shortUrlWithoutDomain = (new ShortUrl('foo'))->setShortCode('my-cool-slug');
|
$shortUrlWithoutDomain = new ShortUrl('foo', ShortUrlMeta::createFromRawData(['customSlug' => 'my-cool-slug']));
|
||||||
$this->getEntityManager()->persist($shortUrlWithoutDomain);
|
$this->getEntityManager()->persist($shortUrlWithoutDomain);
|
||||||
|
|
||||||
$shortUrlWithDomain = (new ShortUrl(
|
$shortUrlWithDomain = new ShortUrl(
|
||||||
'foo',
|
'foo',
|
||||||
ShortUrlMeta::createFromRawData(['domain' => 'doma.in'])
|
ShortUrlMeta::createFromRawData(['domain' => 'doma.in', 'customSlug' => 'another-slug'])
|
||||||
))->setShortCode('another-slug');
|
);
|
||||||
$this->getEntityManager()->persist($shortUrlWithDomain);
|
$this->getEntityManager()->persist($shortUrlWithDomain);
|
||||||
|
|
||||||
$this->getEntityManager()->flush();
|
$this->getEntityManager()->flush();
|
||||||
|
|
|
@ -19,20 +19,21 @@ use Shlinkio\Shlink\Core\Service\ShortUrl\DeleteShortUrlService;
|
||||||
|
|
||||||
use function Functional\map;
|
use function Functional\map;
|
||||||
use function range;
|
use function range;
|
||||||
|
use function sprintf;
|
||||||
|
|
||||||
class DeleteShortUrlServiceTest extends TestCase
|
class DeleteShortUrlServiceTest extends TestCase
|
||||||
{
|
{
|
||||||
/** @var DeleteShortUrlService */
|
|
||||||
private $service;
|
|
||||||
/** @var ObjectProphecy */
|
/** @var ObjectProphecy */
|
||||||
private $em;
|
private $em;
|
||||||
|
/** @var string */
|
||||||
|
private $shortCode;
|
||||||
|
|
||||||
public function setUp(): void
|
public function setUp(): void
|
||||||
{
|
{
|
||||||
$shortUrl = (new ShortUrl(''))->setShortCode('abc123')
|
$shortUrl = (new ShortUrl(''))->setVisits(new ArrayCollection(map(range(0, 10), function () {
|
||||||
->setVisits(new ArrayCollection(map(range(0, 10), function () {
|
return new Visit(new ShortUrl(''), Visitor::emptyInstance());
|
||||||
return new Visit(new ShortUrl(''), Visitor::emptyInstance());
|
})));
|
||||||
})));
|
$this->shortCode = $shortUrl->getShortCode();
|
||||||
|
|
||||||
$this->em = $this->prophesize(EntityManagerInterface::class);
|
$this->em = $this->prophesize(EntityManagerInterface::class);
|
||||||
|
|
||||||
|
@ -42,55 +43,56 @@ class DeleteShortUrlServiceTest extends TestCase
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
/** @test */
|
||||||
public function deleteByShortCodeThrowsExceptionWhenThresholdIsReached()
|
public function deleteByShortCodeThrowsExceptionWhenThresholdIsReached(): void
|
||||||
{
|
{
|
||||||
$service = $this->createService();
|
$service = $this->createService();
|
||||||
|
|
||||||
$this->expectException(DeleteShortUrlException::class);
|
$this->expectException(DeleteShortUrlException::class);
|
||||||
$this->expectExceptionMessage(
|
$this->expectExceptionMessage(sprintf(
|
||||||
'Impossible to delete short URL with short code "abc123" since it has more than "5" visits.'
|
'Impossible to delete short URL with short code "%s" since it has more than "5" visits.',
|
||||||
);
|
$this->shortCode
|
||||||
|
));
|
||||||
|
|
||||||
$service->deleteByShortCode('abc123');
|
$service->deleteByShortCode($this->shortCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
/** @test */
|
||||||
public function deleteByShortCodeDeletesUrlWhenThresholdIsReachedButExplicitlyIgnored()
|
public function deleteByShortCodeDeletesUrlWhenThresholdIsReachedButExplicitlyIgnored(): void
|
||||||
{
|
{
|
||||||
$service = $this->createService();
|
$service = $this->createService();
|
||||||
|
|
||||||
$remove = $this->em->remove(Argument::type(ShortUrl::class))->willReturn(null);
|
$remove = $this->em->remove(Argument::type(ShortUrl::class))->willReturn(null);
|
||||||
$flush = $this->em->flush()->willReturn(null);
|
$flush = $this->em->flush()->willReturn(null);
|
||||||
|
|
||||||
$service->deleteByShortCode('abc123', true);
|
$service->deleteByShortCode($this->shortCode, true);
|
||||||
|
|
||||||
$remove->shouldHaveBeenCalledOnce();
|
$remove->shouldHaveBeenCalledOnce();
|
||||||
$flush->shouldHaveBeenCalledOnce();
|
$flush->shouldHaveBeenCalledOnce();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
/** @test */
|
||||||
public function deleteByShortCodeDeletesUrlWhenThresholdIsReachedButCheckIsDisabled()
|
public function deleteByShortCodeDeletesUrlWhenThresholdIsReachedButCheckIsDisabled(): void
|
||||||
{
|
{
|
||||||
$service = $this->createService(false);
|
$service = $this->createService(false);
|
||||||
|
|
||||||
$remove = $this->em->remove(Argument::type(ShortUrl::class))->willReturn(null);
|
$remove = $this->em->remove(Argument::type(ShortUrl::class))->willReturn(null);
|
||||||
$flush = $this->em->flush()->willReturn(null);
|
$flush = $this->em->flush()->willReturn(null);
|
||||||
|
|
||||||
$service->deleteByShortCode('abc123');
|
$service->deleteByShortCode($this->shortCode);
|
||||||
|
|
||||||
$remove->shouldHaveBeenCalledOnce();
|
$remove->shouldHaveBeenCalledOnce();
|
||||||
$flush->shouldHaveBeenCalledOnce();
|
$flush->shouldHaveBeenCalledOnce();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
/** @test */
|
||||||
public function deleteByShortCodeDeletesUrlWhenThresholdIsNotReached()
|
public function deleteByShortCodeDeletesUrlWhenThresholdIsNotReached(): void
|
||||||
{
|
{
|
||||||
$service = $this->createService(true, 100);
|
$service = $this->createService(true, 100);
|
||||||
|
|
||||||
$remove = $this->em->remove(Argument::type(ShortUrl::class))->willReturn(null);
|
$remove = $this->em->remove(Argument::type(ShortUrl::class))->willReturn(null);
|
||||||
$flush = $this->em->flush()->willReturn(null);
|
$flush = $this->em->flush()->willReturn(null);
|
||||||
|
|
||||||
$service->deleteByShortCode('abc123');
|
$service->deleteByShortCode($this->shortCode);
|
||||||
|
|
||||||
$remove->shouldHaveBeenCalledOnce();
|
$remove->shouldHaveBeenCalledOnce();
|
||||||
$flush->shouldHaveBeenCalledOnce();
|
$flush->shouldHaveBeenCalledOnce();
|
||||||
|
|
|
@ -17,7 +17,6 @@ use Prophecy\Argument;
|
||||||
use Prophecy\Prophecy\ObjectProphecy;
|
use Prophecy\Prophecy\ObjectProphecy;
|
||||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||||
use Shlinkio\Shlink\Core\Entity\Tag;
|
use Shlinkio\Shlink\Core\Entity\Tag;
|
||||||
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
|
|
||||||
use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
|
use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
|
||||||
use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException;
|
use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException;
|
||||||
use Shlinkio\Shlink\Core\Exception\RuntimeException;
|
use Shlinkio\Shlink\Core\Exception\RuntimeException;
|
||||||
|
@ -74,13 +73,13 @@ class UrlShortenerTest extends TestCase
|
||||||
/** @test */
|
/** @test */
|
||||||
public function urlIsProperlyShortened(): void
|
public function urlIsProperlyShortened(): void
|
||||||
{
|
{
|
||||||
// 10 -> 0Q1Y
|
|
||||||
$shortUrl = $this->urlShortener->urlToShortCode(
|
$shortUrl = $this->urlShortener->urlToShortCode(
|
||||||
new Uri('http://foobar.com/12345/hello?foo=bar'),
|
new Uri('http://foobar.com/12345/hello?foo=bar'),
|
||||||
[],
|
[],
|
||||||
ShortUrlMeta::createEmpty()
|
ShortUrlMeta::createEmpty()
|
||||||
);
|
);
|
||||||
$this->assertEquals('0Q1Y', $shortUrl->getShortCode());
|
|
||||||
|
$this->assertEquals('http://foobar.com/12345/hello?foo=bar', $shortUrl->getLongUrl());
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
/** @test */
|
||||||
|
@ -243,9 +242,8 @@ class UrlShortenerTest extends TestCase
|
||||||
/** @test */
|
/** @test */
|
||||||
public function shortCodeIsProperlyParsed(): void
|
public function shortCodeIsProperlyParsed(): void
|
||||||
{
|
{
|
||||||
$shortCode = '12C1c';
|
|
||||||
$shortUrl = new ShortUrl('expected_url');
|
$shortUrl = new ShortUrl('expected_url');
|
||||||
$shortUrl->setShortCode($shortCode);
|
$shortCode = $shortUrl->getShortCode();
|
||||||
|
|
||||||
$repo = $this->prophesize(ShortUrlRepositoryInterface::class);
|
$repo = $this->prophesize(ShortUrlRepositoryInterface::class);
|
||||||
$repo->findOneByShortCode($shortCode, null)->willReturn($shortUrl);
|
$repo->findOneByShortCode($shortCode, null)->willReturn($shortUrl);
|
||||||
|
@ -254,11 +252,4 @@ class UrlShortenerTest extends TestCase
|
||||||
$url = $this->urlShortener->shortCodeToUrl($shortCode);
|
$url = $this->urlShortener->shortCodeToUrl($shortCode);
|
||||||
$this->assertSame($shortUrl, $url);
|
$this->assertSame($shortUrl, $url);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
|
||||||
public function invalidCharSetThrowsException(): void
|
|
||||||
{
|
|
||||||
$this->expectException(InvalidShortCodeException::class);
|
|
||||||
$this->urlShortener->shortCodeToUrl('&/(');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -90,7 +90,7 @@ class CreateShortUrlActionTest extends ApiTestCase
|
||||||
|
|
||||||
$this->assertEquals(self::STATUS_OK, $statusCode);
|
$this->assertEquals(self::STATUS_OK, $statusCode);
|
||||||
|
|
||||||
// Request to the short URL will return a 404 since ist' not valid yet
|
// Request to the short URL will return a 404 since it's not valid yet
|
||||||
$lastResp = $this->callShortUrl($shortCode);
|
$lastResp = $this->callShortUrl($shortCode);
|
||||||
$this->assertEquals(self::STATUS_NOT_FOUND, $lastResp->getStatusCode());
|
$this->assertEquals(self::STATUS_NOT_FOUND, $lastResp->getStatusCode());
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,13 +20,15 @@ class ShortUrlsFixture extends AbstractFixture
|
||||||
*/
|
*/
|
||||||
public function load(ObjectManager $manager): void
|
public function load(ObjectManager $manager): void
|
||||||
{
|
{
|
||||||
$abcShortUrl = $this->setShortUrlDate(new ShortUrl('https://shlink.io'))->setShortCode('abc123');
|
$abcShortUrl = $this->setShortUrlDate(
|
||||||
|
new ShortUrl('https://shlink.io', ShortUrlMeta::createFromRawData(['customSlug' => 'abc123']))
|
||||||
|
);
|
||||||
$manager->persist($abcShortUrl);
|
$manager->persist($abcShortUrl);
|
||||||
|
|
||||||
$defShortUrl = $this->setShortUrlDate(new ShortUrl(
|
$defShortUrl = $this->setShortUrlDate(new ShortUrl(
|
||||||
'https://blog.alejandrocelaya.com/2017/12/09/acmailer-7-0-the-most-important-release-in-a-long-time/',
|
'https://blog.alejandrocelaya.com/2017/12/09/acmailer-7-0-the-most-important-release-in-a-long-time/',
|
||||||
ShortUrlMeta::createFromParams(Chronos::parse('2020-05-01'))
|
ShortUrlMeta::createFromParams(Chronos::parse('2020-05-01'), null, 'def456')
|
||||||
))->setShortCode('def456');
|
));
|
||||||
$manager->persist($defShortUrl);
|
$manager->persist($defShortUrl);
|
||||||
|
|
||||||
$customShortUrl = $this->setShortUrlDate(new ShortUrl(
|
$customShortUrl = $this->setShortUrlDate(new ShortUrl(
|
||||||
|
@ -37,14 +39,14 @@ class ShortUrlsFixture extends AbstractFixture
|
||||||
|
|
||||||
$withDomainShortUrl = $this->setShortUrlDate(new ShortUrl(
|
$withDomainShortUrl = $this->setShortUrlDate(new ShortUrl(
|
||||||
'https://blog.alejandrocelaya.com/2019/04/27/considerations-to-properly-use-open-source-software-projects/',
|
'https://blog.alejandrocelaya.com/2019/04/27/considerations-to-properly-use-open-source-software-projects/',
|
||||||
ShortUrlMeta::createFromRawData(['domain' => 'example.com'])
|
ShortUrlMeta::createFromRawData(['domain' => 'example.com', 'customSlug' => 'ghi789'])
|
||||||
))->setShortCode('ghi789');
|
));
|
||||||
$manager->persist($withDomainShortUrl);
|
$manager->persist($withDomainShortUrl);
|
||||||
|
|
||||||
$withDomainAndSlugShortUrl = $this->setShortUrlDate(new ShortUrl(
|
$withDomainAndSlugShortUrl = $this->setShortUrlDate(new ShortUrl(
|
||||||
'https://google.com',
|
'https://google.com',
|
||||||
ShortUrlMeta::createFromRawData(['domain' => 'some-domain.com'])
|
ShortUrlMeta::createFromRawData(['domain' => 'some-domain.com', 'customSlug' => 'custom-with-domain'])
|
||||||
))->setShortCode('custom-with-domain');
|
));
|
||||||
$manager->persist($withDomainAndSlugShortUrl);
|
$manager->persist($withDomainAndSlugShortUrl);
|
||||||
|
|
||||||
$manager->flush();
|
$manager->flush();
|
||||||
|
|
|
@ -22,6 +22,11 @@ use function strpos;
|
||||||
|
|
||||||
class CreateShortUrlActionTest extends TestCase
|
class CreateShortUrlActionTest extends TestCase
|
||||||
{
|
{
|
||||||
|
private const DOMAIN_CONFIG = [
|
||||||
|
'schema' => 'http',
|
||||||
|
'hostname' => 'foo.com',
|
||||||
|
];
|
||||||
|
|
||||||
/** @var CreateShortUrlAction */
|
/** @var CreateShortUrlAction */
|
||||||
private $action;
|
private $action;
|
||||||
/** @var ObjectProphecy */
|
/** @var ObjectProphecy */
|
||||||
|
@ -30,10 +35,7 @@ class CreateShortUrlActionTest extends TestCase
|
||||||
public function setUp(): void
|
public function setUp(): void
|
||||||
{
|
{
|
||||||
$this->urlShortener = $this->prophesize(UrlShortener::class);
|
$this->urlShortener = $this->prophesize(UrlShortener::class);
|
||||||
$this->action = new CreateShortUrlAction($this->urlShortener->reveal(), [
|
$this->action = new CreateShortUrlAction($this->urlShortener->reveal(), self::DOMAIN_CONFIG);
|
||||||
'schema' => 'http',
|
|
||||||
'hostname' => 'foo.com',
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
/** @test */
|
||||||
|
@ -46,10 +48,9 @@ class CreateShortUrlActionTest extends TestCase
|
||||||
/** @test */
|
/** @test */
|
||||||
public function properShortcodeConversionReturnsData(): void
|
public function properShortcodeConversionReturnsData(): void
|
||||||
{
|
{
|
||||||
|
$shortUrl = new ShortUrl('');
|
||||||
$this->urlShortener->urlToShortCode(Argument::type(Uri::class), Argument::type('array'), Argument::cetera())
|
$this->urlShortener->urlToShortCode(Argument::type(Uri::class), Argument::type('array'), Argument::cetera())
|
||||||
->willReturn(
|
->willReturn($shortUrl)
|
||||||
(new ShortUrl(''))->setShortCode('abc123')
|
|
||||||
)
|
|
||||||
->shouldBeCalledOnce();
|
->shouldBeCalledOnce();
|
||||||
|
|
||||||
$request = (new ServerRequest())->withParsedBody([
|
$request = (new ServerRequest())->withParsedBody([
|
||||||
|
@ -57,7 +58,7 @@ class CreateShortUrlActionTest extends TestCase
|
||||||
]);
|
]);
|
||||||
$response = $this->action->handle($request);
|
$response = $this->action->handle($request);
|
||||||
$this->assertEquals(200, $response->getStatusCode());
|
$this->assertEquals(200, $response->getStatusCode());
|
||||||
$this->assertTrue(strpos($response->getBody()->getContents(), 'http://foo.com/abc123') > 0);
|
$this->assertTrue(strpos($response->getBody()->getContents(), $shortUrl->toString(self::DOMAIN_CONFIG)) > 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
/** @test */
|
||||||
|
|
Loading…
Reference in a new issue