diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 4d90fc81..00000000 --- a/.travis.yml +++ /dev/null @@ -1,28 +0,0 @@ -sudo: false - -language: php - -matrix: - fast_finish: true - include: - - php: 5.5 - - php: 5.6 - env: - - EXECUTE_CS_CHECK=true - - php: 7 - - php: hhvm - allow_failures: - - php: hhvm - -before_install: - - composer self-update - -install: - - travis_retry composer install --no-interaction --ignore-platform-reqs --prefer-source --no-scripts - -script: - - composer test - - if [[ $EXECUTE_CS_CHECK == 'true' ]]; then composer cs ; fi - -notifications: - email: true diff --git a/composer.json b/composer.json index 96fc1489..a1b5cf56 100644 --- a/composer.json +++ b/composer.json @@ -47,6 +47,6 @@ "cs-fix": "phpcbf", "serve": "php -S 0.0.0.0:8000 -t public/", "test": "phpunit", - "pretty-test": "phpunit -c tests/phpunit.xml --coverage-html build/coverage" + "pretty-test": "phpunit --coverage-html build/coverage" } } diff --git a/phpunit.xml.dist b/phpunit.xml.dist index bb69885f..0721b191 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,7 +1,7 @@ - - ./test + + ./tests diff --git a/src/Entity/ShortUrl.php b/src/Entity/ShortUrl.php index cdf7c983..994f2e2f 100644 --- a/src/Entity/ShortUrl.php +++ b/src/Entity/ShortUrl.php @@ -41,9 +41,9 @@ class ShortUrl extends AbstractEntity */ public function __construct() { - $this->dateCreated = new \DateTime(); - $this->visits = new ArrayCollection(); - $this->shortCode = ''; + $this->setDateCreated(new \DateTime()); + $this->setVisits(new ArrayCollection()); + $this->setShortCode(''); } /** diff --git a/src/Exception/InvalidShortCodeException.php b/src/Exception/InvalidShortCodeException.php new file mode 100644 index 00000000..b41038a1 --- /dev/null +++ b/src/Exception/InvalidShortCodeException.php @@ -0,0 +1,15 @@ +getCode() : -1; + return new static( + sprintf('Provided short code "%s" does not match the char set "%s"', $shortCode, $charSet), + $code, + $previous + ); + } +} diff --git a/src/Service/UrlShortener.php b/src/Service/UrlShortener.php index 0364bf5c..55953691 100644 --- a/src/Service/UrlShortener.php +++ b/src/Service/UrlShortener.php @@ -2,6 +2,7 @@ namespace Acelaya\UrlShortener\Service; use Acelaya\UrlShortener\Entity\ShortUrl; +use Acelaya\UrlShortener\Exception\InvalidShortCodeException; use Acelaya\UrlShortener\Exception\InvalidUrlException; use Acelaya\UrlShortener\Exception\RuntimeException; use Doctrine\ORM\EntityManagerInterface; @@ -12,7 +13,7 @@ use Psr\Http\Message\UriInterface; class UrlShortener implements UrlShortenerInterface { - const DEFAULT_CHARS = '123456789bcdfghjkmnpqrstvwxyzBCDFGHJKLMNPQRSTVWXYZ'; + const DEFAULT_CHARS = 'rYHxLkXfsptbNZzKDG4hy85WFT7BRgMVdC9jvwQPnc6S32Jqm'; /** * @var ClientInterface @@ -38,6 +39,8 @@ class UrlShortener implements UrlShortenerInterface } /** + * Creates and persists a unique shortcode generated for provided url + * * @param UriInterface $url * @return string * @throws InvalidUrlException @@ -106,26 +109,37 @@ class UrlShortener implements UrlShortenerInterface */ protected function convertAutoincrementIdToShortCode($id) { - $id = intval($id); + $id = intval($id) + 200000; // Increment the Id so that the generated shortcode is not too short $length = strlen($this->chars); $code = ''; - while ($id > $length - 1) { + while ($id > 0) { // Determine the value of the next higher character in the short code and prepend it - $code = $this->chars[fmod($id, $length)] . $code; + $code = $this->chars[intval(fmod($id, $length))] . $code; $id = floor($id / $length); } - return $this->chars[$id] . $code; + return $this->chars[intval($id)] . $code; } /** + * Tries to find the mapped URL for provided short code. Returns null if not found + * * @param string $shortCode - * @return string + * @return string|null + * @throws InvalidShortCodeException */ public function shortCodeToUrl($shortCode) { // Validate short code format - + if (! preg_match('|[' . $this->chars . "]+|", $shortCode)) { + throw InvalidShortCodeException::fromShortCode($shortCode, $this->chars); + } + + /** @var ShortUrl $shortUrl */ + $shortUrl = $this->em->getRepository(ShortUrl::class)->findOneBy([ + 'shortCode' => $shortCode, + ]); + return isset($shortUrl) ? $shortUrl->getOriginalUrl() : null; } } diff --git a/src/Service/UrlShortenerInterface.php b/src/Service/UrlShortenerInterface.php index 6c211c10..6623f392 100644 --- a/src/Service/UrlShortenerInterface.php +++ b/src/Service/UrlShortenerInterface.php @@ -1,6 +1,7 @@ httpClient = $this->prophesize(ClientInterface::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) { + /** @var ShortUrl $shortUrl */ + $shortUrl = $arguments[0]; + $shortUrl->setId(10); + }); + $repo = $this->prophesize(ObjectRepository::class); + $repo->findOneBy(Argument::any())->willReturn(null); + $this->em->getRepository(ShortUrl::class)->willReturn($repo->reveal()); + + $this->urlShortener = new UrlShortener($this->httpClient->reveal(), $this->em->reveal()); + } + + /** + * @test + */ + public function urlIsProperlyShortened() + { + // 10 -> rY9zc + $shortCode = $this->urlShortener->urlToShortCode(new Uri('http://foobar.com/12345/hello?foo=bar')); + $this->assertEquals('rY9zc', $shortCode); + } + + /** + * @test + * @expectedException \Acelaya\UrlShortener\Exception\RuntimeException + */ + public function exceptionIsThrownWhenOrmThrowsException() + { + $conn = $this->prophesize(Connection::class); + $conn->isTransactionActive()->willReturn(true); + $this->em->getConnection()->willReturn($conn->reveal()); + $this->em->rollback()->shouldBeCalledTimes(1); + $this->em->close()->shouldBeCalledTimes(1); + + $this->em->flush()->willThrow(new ORMException()); + $this->urlShortener->urlToShortCode(new Uri('http://foobar.com/12345/hello?foo=bar')); + } + + /** + * @test + * @expectedException \Acelaya\UrlShortener\Exception\InvalidUrlException + */ + public function exceptionIsThrownWhenUrlDoesNotExist() + { + $this->httpClient->request(Argument::cetera())->willThrow( + new ClientException('', $this->prophesize(Request::class)->reveal()) + ); + $this->urlShortener->urlToShortCode(new Uri('http://foobar.com/12345/hello?foo=bar')); + } + + /** + * @test + */ + public function whenShortUrlExistsItsShortcodeIsReturned() + { + $shortUrl = new ShortUrl(); + $shortUrl->setShortCode('expected_shortcode'); + $repo = $this->prophesize(ObjectRepository::class); + $repo->findOneBy(Argument::any())->willReturn($shortUrl); + $this->em->getRepository(ShortUrl::class)->willReturn($repo->reveal()); + + $shortCode = $this->urlShortener->urlToShortCode(new Uri('http://foobar.com/12345/hello?foo=bar')); + $this->assertEquals($shortUrl->getShortCode(), $shortCode); + } + + /** + * @test + */ + public function shortCodeIsProperlyParsed() + { + // rY9zc -> 10 + $shortUrl = new ShortUrl(); + $shortUrl->setShortCode('rY9zc') + ->setOriginalUrl('expected_url'); + + $repo = $this->prophesize(ObjectRepository::class); + $repo->findOneBy(['shortCode' => 'rY9zc'])->willReturn($shortUrl); + $this->em->getRepository(ShortUrl::class)->willReturn($repo->reveal()); + + $url = $this->urlShortener->shortCodeToUrl('rY9zc'); + $this->assertEquals($shortUrl->getOriginalUrl(), $url); + } + + /** + * @test + * @expectedException \Acelaya\UrlShortener\Exception\InvalidShortCodeException + */ + public function invalidCharSetThrowsException() + { + $this->urlShortener->shortCodeToUrl('&/('); + } +}