123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277 |
- <?php
- declare(strict_types=1);
- namespace ShlinkioTest\Shlink\Core\Service;
- use Cake\Chronos\Chronos;
- use Doctrine\Common\Collections\ArrayCollection;
- use Doctrine\DBAL\Connection;
- use Doctrine\ORM\EntityManagerInterface;
- use Doctrine\ORM\ORMException;
- use Laminas\Diactoros\Uri;
- use PHPUnit\Framework\TestCase;
- use Prophecy\Argument;
- use Prophecy\Prophecy\ObjectProphecy;
- use Shlinkio\Shlink\Core\Entity\ShortUrl;
- use Shlinkio\Shlink\Core\Entity\Tag;
- use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException;
- use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
- use Shlinkio\Shlink\Core\Options\UrlShortenerOptions;
- use Shlinkio\Shlink\Core\Repository\ShortUrlRepository;
- use Shlinkio\Shlink\Core\Repository\ShortUrlRepositoryInterface;
- use Shlinkio\Shlink\Core\Service\UrlShortener;
- use Shlinkio\Shlink\Core\Util\UrlValidatorInterface;
- use function array_map;
- class UrlShortenerTest extends TestCase
- {
- private UrlShortener $urlShortener;
- private ObjectProphecy $em;
- private ObjectProphecy $urlValidator;
- public function setUp(): void
- {
- $this->urlValidator = $this->prophesize(UrlValidatorInterface::class);
- $this->em = $this->prophesize(EntityManagerInterface::class);
- $conn = $this->prophesize(Connection::class);
- $conn->isTransactionActive()->willReturn(false);
- $this->em->getConnection()->willReturn($conn->reveal());
- $this->em->flush()->willReturn(null);
- $this->em->commit()->willReturn(null);
- $this->em->beginTransaction()->willReturn(null);
- $this->em->persist(Argument::any())->will(function ($arguments): void {
- /** @var ShortUrl $shortUrl */
- [$shortUrl] = $arguments;
- $shortUrl->setId('10');
- });
- $repo = $this->prophesize(ShortUrlRepository::class);
- $repo->shortCodeIsInUse(Argument::cetera())->willReturn(false);
- $this->em->getRepository(ShortUrl::class)->willReturn($repo->reveal());
- $this->setUrlShortener(false);
- }
- private function setUrlShortener(bool $urlValidationEnabled): void
- {
- $this->urlShortener = new UrlShortener(
- $this->urlValidator->reveal(),
- $this->em->reveal(),
- new UrlShortenerOptions(['validate_url' => $urlValidationEnabled]),
- );
- }
- /** @test */
- public function urlIsProperlyShortened(): void
- {
- $shortUrl = $this->urlShortener->urlToShortCode(
- new Uri('http://foobar.com/12345/hello?foo=bar'),
- [],
- ShortUrlMeta::createEmpty(),
- );
- $this->assertEquals('http://foobar.com/12345/hello?foo=bar', $shortUrl->getLongUrl());
- }
- /** @test */
- public function shortCodeIsRegeneratedIfAlreadyInUse(): void
- {
- $callIndex = 0;
- $expectedCalls = 3;
- $repo = $this->prophesize(ShortUrlRepository::class);
- $shortCodeIsInUse = $repo->shortCodeIsInUse(Argument::cetera())->will(
- function () use (&$callIndex, $expectedCalls) {
- $callIndex++;
- return $callIndex < $expectedCalls;
- },
- );
- $repo->findBy(Argument::cetera())->willReturn([]);
- $getRepo = $this->em->getRepository(ShortUrl::class)->willReturn($repo->reveal());
- $shortUrl = $this->urlShortener->urlToShortCode(
- new Uri('http://foobar.com/12345/hello?foo=bar'),
- [],
- ShortUrlMeta::createEmpty(),
- );
- $this->assertEquals('http://foobar.com/12345/hello?foo=bar', $shortUrl->getLongUrl());
- $getRepo->shouldBeCalledTimes($expectedCalls);
- $shortCodeIsInUse->shouldBeCalledTimes($expectedCalls);
- }
- /** @test */
- public function transactionIsRolledBackAndExceptionRethrownWhenExceptionIsThrown(): void
- {
- $conn = $this->prophesize(Connection::class);
- $conn->isTransactionActive()->willReturn(true);
- $this->em->getConnection()->willReturn($conn->reveal());
- $this->em->rollback()->shouldBeCalledOnce();
- $this->em->close()->shouldBeCalledOnce();
- $this->em->flush()->willThrow(new ORMException());
- $this->expectException(ORMException::class);
- $this->urlShortener->urlToShortCode(
- new Uri('http://foobar.com/12345/hello?foo=bar'),
- [],
- ShortUrlMeta::createEmpty(),
- );
- }
- /** @test */
- public function validatorIsCalledWhenUrlValidationIsEnabled(): void
- {
- $this->setUrlShortener(true);
- $validateUrl = $this->urlValidator->validateUrl('http://foobar.com/12345/hello?foo=bar')->will(
- function (): void {
- },
- );
- $this->urlShortener->urlToShortCode(
- new Uri('http://foobar.com/12345/hello?foo=bar'),
- [],
- ShortUrlMeta::createEmpty(),
- );
- $validateUrl->shouldHaveBeenCalledOnce();
- }
- /** @test */
- public function exceptionIsThrownWhenNonUniqueSlugIsProvided(): void
- {
- $repo = $this->prophesize(ShortUrlRepository::class);
- $shortCodeIsInUse = $repo->shortCodeIsInUse('custom-slug', null)->willReturn(true);
- $repo->findBy(Argument::cetera())->willReturn([]);
- $getRepo = $this->em->getRepository(ShortUrl::class)->willReturn($repo->reveal());
- $shortCodeIsInUse->shouldBeCalledOnce();
- $getRepo->shouldBeCalled();
- $this->expectException(NonUniqueSlugException::class);
- $this->urlShortener->urlToShortCode(
- new Uri('http://foobar.com/12345/hello?foo=bar'),
- [],
- ShortUrlMeta::createFromRawData(['customSlug' => 'custom-slug']),
- );
- }
- /**
- * @test
- * @dataProvider provideExistingShortUrls
- */
- public function existingShortUrlIsReturnedWhenRequested(
- string $url,
- array $tags,
- ShortUrlMeta $meta,
- ShortUrl $expected
- ): void {
- $repo = $this->prophesize(ShortUrlRepository::class);
- $findExisting = $repo->findBy(Argument::any())->willReturn([$expected]);
- $getRepo = $this->em->getRepository(ShortUrl::class)->willReturn($repo->reveal());
- $result = $this->urlShortener->urlToShortCode(new Uri($url), $tags, $meta);
- $findExisting->shouldHaveBeenCalledOnce();
- $getRepo->shouldHaveBeenCalledOnce();
- $this->em->persist(Argument::cetera())->shouldNotHaveBeenCalled();
- $this->assertSame($expected, $result);
- }
- public function provideExistingShortUrls(): iterable
- {
- $url = 'http://foo.com';
- yield [$url, [], ShortUrlMeta::createFromRawData(['findIfExists' => true]), new ShortUrl($url)];
- yield [$url, [], ShortUrlMeta::createFromRawData(
- ['findIfExists' => true, 'customSlug' => 'foo'],
- ), new ShortUrl($url)];
- yield [
- $url,
- ['foo', 'bar'],
- ShortUrlMeta::createFromRawData(['findIfExists' => true]),
- (new ShortUrl($url))->setTags(new ArrayCollection([new Tag('bar'), new Tag('foo')])),
- ];
- yield [
- $url,
- [],
- ShortUrlMeta::createFromRawData(['findIfExists' => true, 'maxVisits' => 3]),
- new ShortUrl($url, ShortUrlMeta::createFromRawData(['maxVisits' => 3])),
- ];
- yield [
- $url,
- [],
- ShortUrlMeta::createFromRawData(['findIfExists' => true, 'validSince' => Chronos::parse('2017-01-01')]),
- new ShortUrl($url, ShortUrlMeta::createFromRawData(['validSince' => Chronos::parse('2017-01-01')])),
- ];
- yield [
- $url,
- [],
- ShortUrlMeta::createFromRawData(['findIfExists' => true, 'validUntil' => Chronos::parse('2017-01-01')]),
- new ShortUrl($url, ShortUrlMeta::createFromRawData(['validUntil' => Chronos::parse('2017-01-01')])),
- ];
- yield [
- $url,
- [],
- ShortUrlMeta::createFromRawData(['findIfExists' => true, 'domain' => 'example.com']),
- new ShortUrl($url, ShortUrlMeta::createFromRawData(['domain' => 'example.com'])),
- ];
- yield [
- $url,
- ['baz', 'foo', 'bar'],
- ShortUrlMeta::createFromRawData([
- 'findIfExists' => true,
- 'validUntil' => Chronos::parse('2017-01-01'),
- 'maxVisits' => 4,
- ]),
- (new ShortUrl($url, ShortUrlMeta::createFromRawData([
- 'validUntil' => Chronos::parse('2017-01-01'),
- 'maxVisits' => 4,
- ])))->setTags(new ArrayCollection([new Tag('foo'), new Tag('bar'), new Tag('baz')])),
- ];
- }
- /** @test */
- public function properExistingShortUrlIsReturnedWhenMultipleMatch(): void
- {
- $url = 'http://foo.com';
- $tags = ['baz', 'foo', 'bar'];
- $meta = ShortUrlMeta::createFromRawData([
- 'findIfExists' => true,
- 'validUntil' => Chronos::parse('2017-01-01'),
- 'maxVisits' => 4,
- ]);
- $tagsCollection = new ArrayCollection(array_map(fn (string $tag) => new Tag($tag), $tags));
- $expected = (new ShortUrl($url, $meta))->setTags($tagsCollection);
- $repo = $this->prophesize(ShortUrlRepository::class);
- $findExisting = $repo->findBy(Argument::any())->willReturn([
- new ShortUrl($url),
- new ShortUrl($url, $meta),
- $expected,
- (new ShortUrl($url))->setTags($tagsCollection),
- ]);
- $getRepo = $this->em->getRepository(ShortUrl::class)->willReturn($repo->reveal());
- $result = $this->urlShortener->urlToShortCode(new Uri($url), $tags, $meta);
- $this->assertSame($expected, $result);
- $findExisting->shouldHaveBeenCalledOnce();
- $getRepo->shouldHaveBeenCalledOnce();
- }
- /** @test */
- public function shortCodeIsProperlyParsed(): void
- {
- $shortUrl = new ShortUrl('expected_url');
- $shortCode = $shortUrl->getShortCode();
- $repo = $this->prophesize(ShortUrlRepositoryInterface::class);
- $repo->findOneByShortCode($shortCode, null)->willReturn($shortUrl);
- $this->em->getRepository(ShortUrl::class)->willReturn($repo->reveal());
- $url = $this->urlShortener->shortCodeToUrl($shortCode);
- $this->assertSame($shortUrl, $url);
- }
- }
|