From c569cef23912189bf1513f0ad55f83b77c20477b Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Fri, 29 Jul 2016 11:25:53 +0200 Subject: [PATCH 01/25] Fixed ContentBased error handler not using the default content if accepted contents are not valid --- .../src/Expressive/ContentBasedErrorHandler.php | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/module/Common/src/Expressive/ContentBasedErrorHandler.php b/module/Common/src/Expressive/ContentBasedErrorHandler.php index 60e1dfdb..7dbe2509 100644 --- a/module/Common/src/Expressive/ContentBasedErrorHandler.php +++ b/module/Common/src/Expressive/ContentBasedErrorHandler.php @@ -47,6 +47,7 @@ class ContentBasedErrorHandler extends AbstractPluginManager implements ErrorHan */ protected function resolveErrorHandlerFromAcceptHeader(Request $request) { + // Try to find an error handler for one of the accepted content types $accepts = $request->hasHeader('Accept') ? $request->getHeaderLine('Accept') : self::DEFAULT_CONTENT; $accepts = explode(',', $accepts); foreach ($accepts as $accept) { @@ -57,8 +58,17 @@ class ContentBasedErrorHandler extends AbstractPluginManager implements ErrorHan return $this->get($accept); } + // If it wasn't possible to find an error handler for accepted content type, use default one if registered + if ($this->has(self::DEFAULT_CONTENT)) { + return $this->get(self::DEFAULT_CONTENT); + } + + // It wasn't possible to find an error handler throw new InvalidArgumentException(sprintf( - 'It wasn\'t possible to find an error handler for ' + 'It wasn\'t possible to find an error handler for ["%s"] content types. ' + . 'Make sure you have registered at least the default "%s" content type', + implode('", "', $accepts), + self::DEFAULT_CONTENT )); } } From 2a018f54155763b5dc53fd1cc17aa3ee89af05ee Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 30 Jul 2016 10:47:29 +0200 Subject: [PATCH 02/25] Fixed ContentBasedErrorHandler fatching error handlers from the composed plugin manager --- module/Common/src/Expressive/ContentBasedErrorHandler.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/module/Common/src/Expressive/ContentBasedErrorHandler.php b/module/Common/src/Expressive/ContentBasedErrorHandler.php index dbd3d900..6f094f69 100644 --- a/module/Common/src/Expressive/ContentBasedErrorHandler.php +++ b/module/Common/src/Expressive/ContentBasedErrorHandler.php @@ -61,8 +61,8 @@ class ContentBasedErrorHandler implements ErrorHandlerInterface } // If it wasn't possible to find an error handler for accepted content type, use default one if registered - if ($this->has(self::DEFAULT_CONTENT)) { - return $this->get(self::DEFAULT_CONTENT); + if ($this->errorHandlerManager->has(self::DEFAULT_CONTENT)) { + return $this->errorHandlerManager->get(self::DEFAULT_CONTENT); } // It wasn't possible to find an error handler From e345c2bbfe3410ad1660aa2ed30a9f348219a9bd Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 30 Jul 2016 13:51:52 +0200 Subject: [PATCH 03/25] Moved error handler classes from Expressive namespace to ErrorHandler namespace --- config/autoload/errorhandler.local.php.dist | 2 +- config/autoload/services.global.php | 7 +------ module/Common/config/error-handler.config.php | 2 +- module/Common/config/services.config.php | 4 ++++ .../ContentBasedErrorHandler.php | 2 +- .../{Expressive => ErrorHandler}/ErrorHandlerInterface.php | 2 +- .../{Expressive => ErrorHandler}/ErrorHandlerManager.php | 2 +- .../ErrorHandlerManagerFactory.php | 2 +- .../ErrorHandlerManagerInterface.php | 2 +- module/Rest/config/error-handler.config.php | 2 +- .../src/{Expressive => ErrorHandler}/JsonErrorHandler.php | 4 ++-- 11 files changed, 15 insertions(+), 16 deletions(-) rename module/Common/src/{Expressive => ErrorHandler}/ContentBasedErrorHandler.php (98%) rename module/Common/src/{Expressive => ErrorHandler}/ErrorHandlerInterface.php (89%) rename module/Common/src/{Expressive => ErrorHandler}/ErrorHandlerManager.php (92%) rename module/Common/src/{Expressive => ErrorHandler}/ErrorHandlerManagerFactory.php (95%) rename module/Common/src/{Expressive => ErrorHandler}/ErrorHandlerManagerInterface.php (72%) rename module/Rest/src/{Expressive => ErrorHandler}/JsonErrorHandler.php (92%) diff --git a/config/autoload/errorhandler.local.php.dist b/config/autoload/errorhandler.local.php.dist index d6de56d7..c65493f2 100644 --- a/config/autoload/errorhandler.local.php.dist +++ b/config/autoload/errorhandler.local.php.dist @@ -1,6 +1,6 @@ InvokableFactory::class, // View - ContentBasedErrorHandler::class => AnnotatedFactory::class, - ErrorHandlerManager::class => ErrorHandlerManagerFactory::class, Template\TemplateRendererInterface::class => Twig\TwigRendererFactory::class, ], 'aliases' => [ diff --git a/module/Common/config/error-handler.config.php b/module/Common/config/error-handler.config.php index a6165fa2..d19b9ac6 100644 --- a/module/Common/config/error-handler.config.php +++ b/module/Common/config/error-handler.config.php @@ -1,5 +1,5 @@ TranslatorFactory::class, TranslatorExtension::class => AnnotatedFactory::class, LocaleMiddleware::class => AnnotatedFactory::class, + + ErrorHandler\ContentBasedErrorHandler::class => AnnotatedFactory::class, + ErrorHandler\ErrorHandlerManager::class => ErrorHandler\ErrorHandlerManagerFactory::class, ], 'aliases' => [ 'em' => EntityManager::class, diff --git a/module/Common/src/Expressive/ContentBasedErrorHandler.php b/module/Common/src/ErrorHandler/ContentBasedErrorHandler.php similarity index 98% rename from module/Common/src/Expressive/ContentBasedErrorHandler.php rename to module/Common/src/ErrorHandler/ContentBasedErrorHandler.php index 6f094f69..be19e848 100644 --- a/module/Common/src/Expressive/ContentBasedErrorHandler.php +++ b/module/Common/src/ErrorHandler/ContentBasedErrorHandler.php @@ -1,5 +1,5 @@ Date: Sat, 30 Jul 2016 13:54:00 +0200 Subject: [PATCH 04/25] Removed whiteline --- config/autoload/errorhandler.local.php.dist | 1 - 1 file changed, 1 deletion(-) diff --git a/config/autoload/errorhandler.local.php.dist b/config/autoload/errorhandler.local.php.dist index c65493f2..afbc52ed 100644 --- a/config/autoload/errorhandler.local.php.dist +++ b/config/autoload/errorhandler.local.php.dist @@ -1,5 +1,4 @@ Date: Sat, 30 Jul 2016 14:12:56 +0200 Subject: [PATCH 05/25] Created GenerateShortcodeCommandTest --- .../src/Command/GenerateShortcodeCommand.php | 6 +- .../Command/GenerateShortcodeCommandTest.php | 70 +++++++++++++++++++ 2 files changed, 73 insertions(+), 3 deletions(-) create mode 100644 module/CLI/test/Command/GenerateShortcodeCommandTest.php diff --git a/module/CLI/src/Command/GenerateShortcodeCommand.php b/module/CLI/src/Command/GenerateShortcodeCommand.php index 0a0af9eb..f02c110b 100644 --- a/module/CLI/src/Command/GenerateShortcodeCommand.php +++ b/module/CLI/src/Command/GenerateShortcodeCommand.php @@ -52,7 +52,7 @@ class GenerateShortcodeCommand extends Command { $this->setName('shortcode:generate') ->setDescription( - $this->translator->translate('Generates a shortcode for provided URL and returns the short URL') + $this->translator->translate('Generates a short code for provided URL and returns the short URL') ) ->addArgument('longUrl', InputArgument::REQUIRED, $this->translator->translate('The long URL to parse')); } @@ -87,8 +87,8 @@ class GenerateShortcodeCommand extends Command return; } - $shortcode = $this->urlShortener->urlToShortCode(new Uri($longUrl)); - $shortUrl = (new Uri())->withPath($shortcode) + $shortCode = $this->urlShortener->urlToShortCode(new Uri($longUrl)); + $shortUrl = (new Uri())->withPath($shortCode) ->withScheme($this->domainConfig['schema']) ->withHost($this->domainConfig['hostname']); diff --git a/module/CLI/test/Command/GenerateShortcodeCommandTest.php b/module/CLI/test/Command/GenerateShortcodeCommandTest.php new file mode 100644 index 00000000..45cb8130 --- /dev/null +++ b/module/CLI/test/Command/GenerateShortcodeCommandTest.php @@ -0,0 +1,70 @@ +urlShortener = $this->prophesize(UrlShortener::class); + $command = new GenerateShortcodeCommand($this->urlShortener->reveal(), Translator::factory([]), [ + 'schema' => 'http', + 'hostname' => 'foo.com' + ]); + $app = new Application(); + $app->add($command); + $this->commandTester = new CommandTester($command); + } + + /** + * @test + */ + public function properShortCodeIsCreatedIfLongUrlIsCorrect() + { + $this->urlShortener->urlToShortCode(Argument::any())->willReturn('abc123') + ->shouldBeCalledTimes(1); + + $this->commandTester->execute([ + 'command' => 'shortcode:generate', + 'longUrl' => 'http://domain.com/foo/bar' + ]); + $output = $this->commandTester->getDisplay(); + $this->assertTrue(strpos($output, 'http://foo.com/abc123') > 0); + } + + /** + * @test + */ + public function exceptionWhileParsingLongUrlOutputsError() + { + $this->urlShortener->urlToShortCode(Argument::any())->willThrow(new InvalidUrlException()) + ->shouldBeCalledTimes(1); + + $this->commandTester->execute([ + 'command' => 'shortcode:generate', + 'longUrl' => 'http://domain.com/invalid' + ]); + $output = $this->commandTester->getDisplay(); + $this->assertTrue( + strpos($output, 'Provided URL "http://domain.com/invalid" is invalid. Try with a different one.') === 0 + ); + } +} From 3923bf06046d7c9ce79d0857e0053300ad0d0aef Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 30 Jul 2016 14:30:30 +0200 Subject: [PATCH 06/25] Created GetVisitsCommandTest --- .../CLI/test/Command/GetVisitsCommandTest.php | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 module/CLI/test/Command/GetVisitsCommandTest.php diff --git a/module/CLI/test/Command/GetVisitsCommandTest.php b/module/CLI/test/Command/GetVisitsCommandTest.php new file mode 100644 index 00000000..4294823b --- /dev/null +++ b/module/CLI/test/Command/GetVisitsCommandTest.php @@ -0,0 +1,91 @@ +visitsTracker = $this->prophesize(VisitsTrackerInterface::class); + $command = new GetVisitsCommand($this->visitsTracker->reveal(), Translator::factory([])); + $app = new Application(); + $app->add($command); + $this->commandTester = new CommandTester($command); + } + + /** + * @test + */ + public function noDateFlagsTriesToListWithoutDateRange() + { + $shortCode = 'abc123'; + $this->visitsTracker->info($shortCode, new DateRange(null, null))->willReturn([]) + ->shouldBeCalledTimes(1); + + $this->commandTester->execute([ + 'command' => 'shortcode:visits', + 'shortCode' => $shortCode, + ]); + } + + /** + * @test + */ + public function providingDateFlagsTheListGetsFiltered() + { + $shortCode = 'abc123'; + $startDate = '2016-01-01'; + $endDate = '2016-02-01'; + $this->visitsTracker->info($shortCode, new DateRange(new \DateTime($startDate), new \DateTime($endDate))) + ->willReturn([]) + ->shouldBeCalledTimes(1); + + $this->commandTester->execute([ + 'command' => 'shortcode:visits', + 'shortCode' => $shortCode, + '--startDate' => $startDate, + '--endDate' => $endDate, + ]); + } + + /** + * @test + */ + public function outputIsProperlyGenerated() + { + $shortCode = 'abc123'; + $this->visitsTracker->info($shortCode, Argument::any())->willReturn([ + (new Visit())->setReferer('foo') + ->setRemoteAddr('1.2.3.4') + ->setUserAgent('bar'), + ])->shouldBeCalledTimes(1); + + $this->commandTester->execute([ + 'command' => 'shortcode:visits', + 'shortCode' => $shortCode, + ]); + $output = $this->commandTester->getDisplay(); + $this->assertTrue(strpos($output, 'foo') > 0); + $this->assertTrue(strpos($output, '1.2.3.4') > 0); + $this->assertTrue(strpos($output, 'bar') > 0); + } +} From 50f15494572a1c185cb719479046bcfe67b38326 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 30 Jul 2016 14:42:09 +0200 Subject: [PATCH 07/25] Created ProcessVisitsCommand --- .../test/Command/ProcessVisitsCommandTest.php | 96 +++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 module/CLI/test/Command/ProcessVisitsCommandTest.php diff --git a/module/CLI/test/Command/ProcessVisitsCommandTest.php b/module/CLI/test/Command/ProcessVisitsCommandTest.php new file mode 100644 index 00000000..23463123 --- /dev/null +++ b/module/CLI/test/Command/ProcessVisitsCommandTest.php @@ -0,0 +1,96 @@ +visitService = $this->prophesize(VisitService::class); + $this->ipResolver = $this->prophesize(IpLocationResolver::class); + $command = new ProcessVisitsCommand( + $this->visitService->reveal(), + $this->ipResolver->reveal(), + Translator::factory([]) + ); + $app = new Application(); + $app->add($command); + + $this->commandTester = new CommandTester($command); + } + + /** + * @test + */ + public function allReturnedVisitsIpsAreProcessed() + { + $visits = [ + (new Visit())->setRemoteAddr('1.2.3.4'), + (new Visit())->setRemoteAddr('4.3.2.1'), + (new Visit())->setRemoteAddr('12.34.56.78'), + ]; + $this->visitService->getUnlocatedVisits()->willReturn($visits) + ->shouldBeCalledTimes(1); + + $this->visitService->saveVisit(Argument::any())->shouldBeCalledTimes(count($visits)); + $this->ipResolver->resolveIpLocation(Argument::any())->willReturn([]) + ->shouldBeCalledTimes(count($visits)); + + $this->commandTester->execute([ + 'command' => 'visit:process', + ]); + $output = $this->commandTester->getDisplay(); + $this->assertTrue(strpos($output, 'Processing IP 1.2.3.4') === 0); + $this->assertTrue(strpos($output, 'Processing IP 4.3.2.1') > 0); + $this->assertTrue(strpos($output, 'Processing IP 12.34.56.78') > 0); + } + + /** + * @test + */ + public function localhostAddressIsIgnored() + { + $visits = [ + (new Visit())->setRemoteAddr('1.2.3.4'), + (new Visit())->setRemoteAddr('4.3.2.1'), + (new Visit())->setRemoteAddr('12.34.56.78'), + (new Visit())->setRemoteAddr('127.0.0.1'), + (new Visit())->setRemoteAddr('127.0.0.1'), + ]; + $this->visitService->getUnlocatedVisits()->willReturn($visits) + ->shouldBeCalledTimes(1); + + $this->visitService->saveVisit(Argument::any())->shouldBeCalledTimes(count($visits) - 2); + $this->ipResolver->resolveIpLocation(Argument::any())->willReturn([]) + ->shouldBeCalledTimes(count($visits) - 2); + + $this->commandTester->execute([ + 'command' => 'visit:process', + ]); + $output = $this->commandTester->getDisplay(); + $this->assertTrue(strpos($output, 'Ignored localhost address') > 0); + } +} From 4c6cc9cd11f65ec2dd4b63297b3dcc7fc4b5570f Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 30 Jul 2016 16:48:02 +0200 Subject: [PATCH 08/25] Created ResolveUrlCommandTest --- module/CLI/src/Factory/ApplicationFactory.php | 2 +- .../test/Command/ResolveUrlCommandTest.php | 85 +++++++++++++++++++ 2 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 module/CLI/test/Command/ResolveUrlCommandTest.php diff --git a/module/CLI/src/Factory/ApplicationFactory.php b/module/CLI/src/Factory/ApplicationFactory.php index 51d5fdb3..a8e24bf3 100644 --- a/module/CLI/src/Factory/ApplicationFactory.php +++ b/module/CLI/src/Factory/ApplicationFactory.php @@ -25,7 +25,7 @@ class ApplicationFactory implements FactoryInterface public function __invoke(ContainerInterface $container, $requestedName, array $options = null) { $config = $container->get('config')['cli']; - $app = new CliApp(); + $app = new CliApp('Shlink', '1.0.0'); $commands = isset($config['commands']) ? $config['commands'] : []; foreach ($commands as $command) { diff --git a/module/CLI/test/Command/ResolveUrlCommandTest.php b/module/CLI/test/Command/ResolveUrlCommandTest.php new file mode 100644 index 00000000..a2f47c92 --- /dev/null +++ b/module/CLI/test/Command/ResolveUrlCommandTest.php @@ -0,0 +1,85 @@ +urlShortener = $this->prophesize(UrlShortener::class); + $command = new ResolveUrlCommand($this->urlShortener->reveal(), Translator::factory([])); + $app = new Application(); + $app->add($command); + + $this->commandTester = new CommandTester($command); + } + + /** + * @test + */ + public function correctShortCodeResolvesUrl() + { + $shortCode = 'abc123'; + $expectedUrl = 'http://domain.com/foo/bar'; + $this->urlShortener->shortCodeToUrl($shortCode)->willReturn($expectedUrl) + ->shouldBeCalledTimes(1); + + $this->commandTester->execute([ + 'command' => 'shortcode:parse', + 'shortCode' => $shortCode, + ]); + $output = $this->commandTester->getDisplay(); + $this->assertEquals('Long URL: ' . $expectedUrl . PHP_EOL, $output); + } + + /** + * @test + */ + public function incorrectShortCodeOutputsErrorMessage() + { + $shortCode = 'abc123'; + $this->urlShortener->shortCodeToUrl($shortCode)->willReturn(null) + ->shouldBeCalledTimes(1); + + $this->commandTester->execute([ + 'command' => 'shortcode:parse', + 'shortCode' => $shortCode, + ]); + $output = $this->commandTester->getDisplay(); + $this->assertEquals('No URL found for short code "' . $shortCode . '"' . PHP_EOL, $output); + } + + /** + * @test + */ + public function wrongShortCodeFormatOutputsErrorMessage() + { + $shortCode = 'abc123'; + $this->urlShortener->shortCodeToUrl($shortCode)->willThrow(new InvalidShortCodeException()) + ->shouldBeCalledTimes(1); + + $this->commandTester->execute([ + 'command' => 'shortcode:parse', + 'shortCode' => $shortCode, + ]); + $output = $this->commandTester->getDisplay(); + $this->assertEquals('Provided short code "' . $shortCode . '" has an invalid format.' . PHP_EOL, $output); + } +} From 00db8a7ea5e46e91af90c8d8152b9f933fce3d31 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 30 Jul 2016 17:07:35 +0200 Subject: [PATCH 09/25] Created Common\ErrorHandler tests --- .../ContentBasedErrorHandlerTest.php | 75 +++++++++++++++++++ .../ErrorHandlerManagerFactoryTest.php | 35 +++++++++ .../ErrorHandler/ErrorHandlerManagerTest.php | 45 +++++++++++ 3 files changed, 155 insertions(+) create mode 100644 module/Common/test/ErrorHandler/ContentBasedErrorHandlerTest.php create mode 100644 module/Common/test/ErrorHandler/ErrorHandlerManagerFactoryTest.php create mode 100644 module/Common/test/ErrorHandler/ErrorHandlerManagerTest.php diff --git a/module/Common/test/ErrorHandler/ContentBasedErrorHandlerTest.php b/module/Common/test/ErrorHandler/ContentBasedErrorHandlerTest.php new file mode 100644 index 00000000..6b480e54 --- /dev/null +++ b/module/Common/test/ErrorHandler/ContentBasedErrorHandlerTest.php @@ -0,0 +1,75 @@ +errorHandler = new ContentBasedErrorHandler(new ErrorHandlerManager(new ServiceManager(), [ + 'factories' => [ + 'text/html' => [$this, 'factory'], + 'application/json' => [$this, 'factory'], + ], + ])); + } + + public function factory($container, $name) + { + return function () use ($name) { + return $name; + }; + } + + /** + * @test + */ + public function correctAcceptHeaderValueInvokesErrorHandler() + { + $request = ServerRequestFactory::fromGlobals()->withHeader('Accept', 'foo/bar,application/json'); + $result = $this->errorHandler->__invoke($request, new Response()); + $this->assertEquals('application/json', $result); + } + + /** + * @test + */ + public function defaultContentTypeIsUsedWhenNoAcceptHeaderisPresent() + { + $request = ServerRequestFactory::fromGlobals(); + $result = $this->errorHandler->__invoke($request, new Response()); + $this->assertEquals('text/html', $result); + } + + /** + * @test + */ + public function defaultContentTypeIsUsedWhenAcceptedContentIsNotSupported() + { + $request = ServerRequestFactory::fromGlobals()->withHeader('Accept', 'foo/bar,text/xml'); + $result = $this->errorHandler->__invoke($request, new Response()); + $this->assertEquals('text/html', $result); + } + + /** + * @test + * @expectedException \Shlinkio\Shlink\Common\Exception\InvalidArgumentException + */ + public function ifNoErrorHandlerIsFoundAnExceptionIsThrown() + { + $this->errorHandler = new ContentBasedErrorHandler(new ErrorHandlerManager(new ServiceManager(), [])); + $request = ServerRequestFactory::fromGlobals()->withHeader('Accept', 'foo/bar,text/xml'); + $result = $this->errorHandler->__invoke($request, new Response()); + } +} diff --git a/module/Common/test/ErrorHandler/ErrorHandlerManagerFactoryTest.php b/module/Common/test/ErrorHandler/ErrorHandlerManagerFactoryTest.php new file mode 100644 index 00000000..be6d4e6d --- /dev/null +++ b/module/Common/test/ErrorHandler/ErrorHandlerManagerFactoryTest.php @@ -0,0 +1,35 @@ +factory = new ErrorHandlerManagerFactory(); + } + + /** + * @test + */ + public function serviceIsCreated() + { + $instance = $this->factory->__invoke(new ServiceManager(['services' => [ + 'config' => [ + 'error_handler' => [ + 'plugins' => [], + ], + ], + ]]), ''); + $this->assertInstanceOf(ErrorHandlerManager::class, $instance); + } +} diff --git a/module/Common/test/ErrorHandler/ErrorHandlerManagerTest.php b/module/Common/test/ErrorHandler/ErrorHandlerManagerTest.php new file mode 100644 index 00000000..4b14f113 --- /dev/null +++ b/module/Common/test/ErrorHandler/ErrorHandlerManagerTest.php @@ -0,0 +1,45 @@ +pluginManager = new ErrorHandlerManager(new ServiceManager(), [ + 'services' => [ + 'foo' => function () { + }, + ], + 'invokables' => [ + 'invalid' => \stdClass::class, + ] + ]); + } + + /** + * @test + */ + public function callablesAreReturned() + { + $instance = $this->pluginManager->get('foo'); + $this->assertInstanceOf(\Closure::class, $instance); + } + + /** + * @test + * @expectedException \Zend\ServiceManager\Exception\InvalidServiceException + */ + public function nonCallablesThrowException() + { + $this->pluginManager->get('invalid'); + } +} From 2ce6c1f44b8062556311de2b8f12629cfb260a8b Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 30 Jul 2016 17:17:21 +0200 Subject: [PATCH 10/25] Added more tests to Common module --- .../test/Factory/TranslatorFactoryTest.php | 31 ++++++++ .../test/Middleware/LocaleMiddlewareTest.php | 71 +++++++++++++++++++ 2 files changed, 102 insertions(+) create mode 100644 module/Common/test/Factory/TranslatorFactoryTest.php create mode 100644 module/Common/test/Middleware/LocaleMiddlewareTest.php diff --git a/module/Common/test/Factory/TranslatorFactoryTest.php b/module/Common/test/Factory/TranslatorFactoryTest.php new file mode 100644 index 00000000..e70f820b --- /dev/null +++ b/module/Common/test/Factory/TranslatorFactoryTest.php @@ -0,0 +1,31 @@ +factory = new TranslatorFactory(); + } + + /** + * @test + */ + public function serviceIsCreated() + { + $instance = $this->factory->__invoke(new ServiceManager(['services' => [ + 'config' => [], + ]]), ''); + $this->assertInstanceOf(Translator::class, $instance); + } +} diff --git a/module/Common/test/Middleware/LocaleMiddlewareTest.php b/module/Common/test/Middleware/LocaleMiddlewareTest.php new file mode 100644 index 00000000..72e6bf77 --- /dev/null +++ b/module/Common/test/Middleware/LocaleMiddlewareTest.php @@ -0,0 +1,71 @@ +translator = Translator::factory(['locale' => 'ru']); + $this->middleware = new LocaleMiddleware($this->translator); + } + + /** + * @test + */ + public function whenNoHeaderIsPresentLocaleIsNotChanged() + { + $this->assertEquals('ru', $this->translator->getLocale()); + $this->middleware->__invoke(ServerRequestFactory::fromGlobals(), new Response(), function ($req, $resp) { + return $resp; + }); + $this->assertEquals('ru', $this->translator->getLocale()); + } + + /** + * @test + */ + public function whenTheHeaderIsPresentLocaleIsChanged() + { + $this->assertEquals('ru', $this->translator->getLocale()); + $request = ServerRequestFactory::fromGlobals()->withHeader('Accept-Language', 'es'); + $this->middleware->__invoke($request, new Response(), function ($req, $resp) { + return $resp; + }); + $this->assertEquals('es', $this->translator->getLocale()); + } + + /** + * @test + */ + public function localeGetsNormalized() + { + $this->assertEquals('ru', $this->translator->getLocale()); + + $request = ServerRequestFactory::fromGlobals()->withHeader('Accept-Language', 'es_ES'); + $this->middleware->__invoke($request, new Response(), function ($req, $resp) { + return $resp; + }); + $this->assertEquals('es', $this->translator->getLocale()); + + $request = ServerRequestFactory::fromGlobals()->withHeader('Accept-Language', 'en-US'); + $this->middleware->__invoke($request, new Response(), function ($req, $resp) { + return $resp; + }); + $this->assertEquals('en', $this->translator->getLocale()); + } +} From fcdcfde04f7a71822f3bc5cee0df532fec29ed71 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 30 Jul 2016 17:45:48 +0200 Subject: [PATCH 11/25] Added missing tests for Common module --- module/Common/test/ConfigProviderTest.php | 31 ++++++++ .../PaginableRepositoryAdapterTest.php | 43 ++++++++++++ .../test/Service/IpLocationResolverTest.php | 56 +++++++++++++++ .../Extension/TranslatorExtensionTest.php | 70 +++++++++++++++++++ module/Common/test/Util/DateRangeTest.php | 32 +++++++++ 5 files changed, 232 insertions(+) create mode 100644 module/Common/test/ConfigProviderTest.php create mode 100644 module/Common/test/Paginator/PaginableRepositoryAdapterTest.php create mode 100644 module/Common/test/Service/IpLocationResolverTest.php create mode 100644 module/Common/test/Twig/Extension/TranslatorExtensionTest.php create mode 100644 module/Common/test/Util/DateRangeTest.php diff --git a/module/Common/test/ConfigProviderTest.php b/module/Common/test/ConfigProviderTest.php new file mode 100644 index 00000000..54c8c65f --- /dev/null +++ b/module/Common/test/ConfigProviderTest.php @@ -0,0 +1,31 @@ +configProvider = new ConfigProvider(); + } + + /** + * @test + */ + public function configIsReturned() + { + $config = $this->configProvider->__invoke(); + + $this->assertArrayHasKey('error_handler', $config); + $this->assertArrayHasKey('middleware_pipeline', $config); + $this->assertArrayHasKey('services', $config); + $this->assertArrayHasKey('twig', $config); + } +} diff --git a/module/Common/test/Paginator/PaginableRepositoryAdapterTest.php b/module/Common/test/Paginator/PaginableRepositoryAdapterTest.php new file mode 100644 index 00000000..1135682f --- /dev/null +++ b/module/Common/test/Paginator/PaginableRepositoryAdapterTest.php @@ -0,0 +1,43 @@ +repo = $this->prophesize(PaginableRepositoryInterface::class); + $this->adapter = new PaginableRepositoryAdapter($this->repo->reveal(), 'search', 'order'); + } + + /** + * @test + */ + public function getItemsFallbacksToFindList() + { + $this->repo->findList(10, 5, 'search', 'order')->shouldBeCalledTimes(1); + $this->adapter->getItems(5, 10); + } + + /** + * @test + */ + public function countFallbacksToCountList() + { + $this->repo->countList('search')->shouldBeCalledTimes(1); + $this->adapter->count(); + } +} diff --git a/module/Common/test/Service/IpLocationResolverTest.php b/module/Common/test/Service/IpLocationResolverTest.php new file mode 100644 index 00000000..e6130569 --- /dev/null +++ b/module/Common/test/Service/IpLocationResolverTest.php @@ -0,0 +1,56 @@ +client = $this->prophesize(Client::class); + $this->ipResolver = new IpLocationResolver($this->client->reveal()); + } + + /** + * @test + */ + public function correctIpReturnsDecodedInfo() + { + $expected = [ + 'foo' => 'bar', + 'baz' => 'foo', + ]; + $response = new Response(); + $response->getBody()->write(json_encode($expected)); + $response->getBody()->rewind(); + + $this->client->get('http://freegeoip.net/json/1.2.3.4')->willReturn($response) + ->shouldBeCalledTimes(1); + $this->assertEquals($expected, $this->ipResolver->resolveIpLocation('1.2.3.4')); + } + + /** + * @test + * @expectedException \Shlinkio\Shlink\Common\Exception\WrongIpException + */ + public function guzzleExceptionThrowsShlinkException() + { + $this->client->get('http://freegeoip.net/json/1.2.3.4')->willThrow(new TransferException()) + ->shouldBeCalledTimes(1); + $this->ipResolver->resolveIpLocation('1.2.3.4'); + } +} diff --git a/module/Common/test/Twig/Extension/TranslatorExtensionTest.php b/module/Common/test/Twig/Extension/TranslatorExtensionTest.php new file mode 100644 index 00000000..06b5a584 --- /dev/null +++ b/module/Common/test/Twig/Extension/TranslatorExtensionTest.php @@ -0,0 +1,70 @@ +translator = $this->prophesize(Translator::class); + $this->extension = new TranslatorExtension($this->translator->reveal()); + } + + /** + * @test + */ + public function extensionNameIsClassName() + { + $this->assertEquals(TranslatorExtension::class, $this->extension->getName()); + } + + /** + * @test + */ + public function properFunctionsAreReturned() + { + $funcs = $this->extension->getFunctions(); + $this->assertCount(2, $funcs); + foreach ($funcs as $func) { + $this->assertInstanceOf(\Twig_SimpleFunction::class, $func); + } + } + + /** + * @test + */ + public function translateFallbacksToTranslator() + { + $this->translator->translate('foo', 'default', null)->shouldBeCalledTimes(1); + $this->extension->translate('foo'); + + $this->translator->translate('bar', 'baz', 'en')->shouldBeCalledTimes(1); + $this->extension->translate('bar', 'baz', 'en'); + } + + /** + * @test + */ + public function translatePluralFallbacksToTranslator() + { + $this->translator->translatePlural('foo', 'bar', 'baz', 'default', null)->shouldBeCalledTimes(1); + $this->extension->translatePlural('foo', 'bar', 'baz'); + + $this->translator->translatePlural('foo', 'bar', 'baz', 'another', 'en')->shouldBeCalledTimes(1); + $this->extension->translatePlural('foo', 'bar', 'baz', 'another', 'en'); + } +} diff --git a/module/Common/test/Util/DateRangeTest.php b/module/Common/test/Util/DateRangeTest.php new file mode 100644 index 00000000..4ed1586e --- /dev/null +++ b/module/Common/test/Util/DateRangeTest.php @@ -0,0 +1,32 @@ +assertNull($range->getStartDate()); + $this->assertNull($range->getEndDate()); + $this->assertTrue($range->isEmpty()); + } + + /** + * @test + */ + public function providedDatesAreSet() + { + $startDate = new \DateTime(); + $endDate = new \DateTime(); + $range = new DateRange($startDate, $endDate); + $this->assertSame($startDate, $range->getStartDate()); + $this->assertSame($endDate, $range->getEndDate()); + $this->assertFalse($range->isEmpty()); + } +} From ebeaa3c64a721ae2e71172b38660fddf4187cfa8 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 30 Jul 2016 20:02:48 +0200 Subject: [PATCH 12/25] Created RedirectActionTest --- module/Core/src/Action/RedirectAction.php | 17 ++- .../Core/test/Action/RedirectActionTest.php | 104 ++++++++++++++++++ 2 files changed, 118 insertions(+), 3 deletions(-) create mode 100644 module/Core/test/Action/RedirectActionTest.php diff --git a/module/Core/src/Action/RedirectAction.php b/module/Core/src/Action/RedirectAction.php index 122f7f29..031aa0fe 100644 --- a/module/Core/src/Action/RedirectAction.php +++ b/module/Core/src/Action/RedirectAction.php @@ -70,10 +70,10 @@ class RedirectAction implements MiddlewareInterface // If provided shortCode does not belong to a valid long URL, dispatch next middleware, which will trigger // a not-found error if (! isset($longUrl)) { - return $out($request, $response->withStatus(404), 'Not found'); + return $this->notFoundResponse($request, $response, $out); } - // Track visit to this shortcode + // Track visit to this short code $this->visitTracker->track($shortCode); // Return a redirect response to the long URL. @@ -81,7 +81,18 @@ class RedirectAction implements MiddlewareInterface return new RedirectResponse($longUrl); } catch (\Exception $e) { // In case of error, dispatch 404 error - return $out($request, $response); + return $this->notFoundResponse($request, $response, $out); } } + + /** + * @param Request $request + * @param Response $response + * @param callable $out + * @return Response + */ + protected function notFoundResponse(Request $request, Response $response, callable $out) + { + return $out($request, $response->withStatus(404), 'Not Found'); + } } diff --git a/module/Core/test/Action/RedirectActionTest.php b/module/Core/test/Action/RedirectActionTest.php new file mode 100644 index 00000000..434189fc --- /dev/null +++ b/module/Core/test/Action/RedirectActionTest.php @@ -0,0 +1,104 @@ +urlShortener = $this->prophesize(UrlShortener::class); + $visitTracker = $this->prophesize(VisitsTracker::class); + $visitTracker->track(Argument::any()); + $this->action = new RedirectAction($this->urlShortener->reveal(), $visitTracker->reveal()); + } + + /** + * @test + */ + public function redirectionIsPerformedToLongUrl() + { + $shortCode = 'abc123'; + $expectedUrl = 'http://domain.com/foo/bar'; + $this->urlShortener->shortCodeToUrl($shortCode)->willReturn($expectedUrl) + ->shouldBeCalledTimes(1); + + $request = ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode); + $response = $this->action->__invoke($request, new Response()); + + $this->assertInstanceOf(Response\RedirectResponse::class, $response); + $this->assertEquals(302, $response->getStatusCode()); + $this->assertTrue($response->hasHeader('Location')); + $this->assertEquals($expectedUrl, $response->getHeaderLine('Location')); + } + + /** + * @test + */ + public function nextErrorMiddlewareIsInvokedIfLongUrlIsNotFound() + { + $shortCode = 'abc123'; + $this->urlShortener->shortCodeToUrl($shortCode)->willReturn(null) + ->shouldBeCalledTimes(1); + + $request = ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode); + $originalResponse = new Response(); + $test = $this; + $this->action->__invoke($request, $originalResponse, function ( + ServerRequestInterface $req, + ResponseInterface $resp, + $error + ) use ( + $test, + $request + ) { + $test->assertSame($request, $req); + $test->assertEquals(404, $resp->getStatusCode()); + $test->assertEquals('Not Found', $error); + }); + } + + /** + * @test + */ + public function nextErrorMiddlewareIsInvokedIfAnExceptionIsThrown() + { + $shortCode = 'abc123'; + $this->urlShortener->shortCodeToUrl($shortCode)->willThrow(\Exception::class) + ->shouldBeCalledTimes(1); + + $request = ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode); + $originalResponse = new Response(); + $test = $this; + $this->action->__invoke($request, $originalResponse, function ( + ServerRequestInterface $req, + ResponseInterface $resp, + $error + ) use ( + $test, + $request + ) { + $test->assertSame($request, $req); + $test->assertEquals(404, $resp->getStatusCode()); + $test->assertEquals('Not Found', $error); + }); + } +} From ce4877d4ac93931d30d8d8fc951a0e31464d10f0 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 30 Jul 2016 22:55:28 +0200 Subject: [PATCH 13/25] Improved VisitsTrackerTest --- .../Core/test/Service/VisitsTrackerTest.php | 50 ++++++++++++++++--- 1 file changed, 44 insertions(+), 6 deletions(-) diff --git a/module/Core/test/Service/VisitsTrackerTest.php b/module/Core/test/Service/VisitsTrackerTest.php index 2a441e04..4d8a68fe 100644 --- a/module/Core/test/Service/VisitsTrackerTest.php +++ b/module/Core/test/Service/VisitsTrackerTest.php @@ -5,11 +5,29 @@ use Doctrine\ORM\EntityManager; use Doctrine\ORM\EntityRepository; use PHPUnit_Framework_TestCase as TestCase; use Prophecy\Argument; +use Prophecy\Prophecy\ObjectProphecy; use Shlinkio\Shlink\Core\Entity\ShortUrl; +use Shlinkio\Shlink\Core\Entity\Visit; +use Shlinkio\Shlink\Core\Repository\VisitRepository; use Shlinkio\Shlink\Core\Service\VisitsTracker; class VisitsTrackerTest extends TestCase { + /** + * @var VisitsTracker + */ + protected $visitsTracker; + /** + * @var ObjectProphecy + */ + protected $em; + + public function setUp() + { + $this->em = $this->prophesize(EntityManager::class); + $this->visitsTracker = new VisitsTracker($this->em->reveal()); + } + /** * @test */ @@ -19,12 +37,32 @@ class VisitsTrackerTest extends TestCase $repo = $this->prophesize(EntityRepository::class); $repo->findOneBy(['shortCode' => $shortCode])->willReturn(new ShortUrl()); - $em = $this->prophesize(EntityManager::class); - $em->getRepository(ShortUrl::class)->willReturn($repo->reveal())->shouldBeCalledTimes(1); - $em->persist(Argument::any())->shouldBeCalledTimes(1); - $em->flush()->shouldBeCalledTimes(1); + $this->em->getRepository(ShortUrl::class)->willReturn($repo->reveal())->shouldBeCalledTimes(1); + $this->em->persist(Argument::any())->shouldBeCalledTimes(1); + $this->em->flush()->shouldBeCalledTimes(1); - $visitsTracker = new VisitsTracker($em->reveal()); - $visitsTracker->track($shortCode); + $this->visitsTracker->track($shortCode); + } + + /** + * @test + */ + public function infoReturnsVisistForCertainShortCode() + { + $shortCode = '123ABC'; + $shortUrl = (new ShortUrl())->setOriginalUrl('http://domain.com/foo/bar'); + $repo = $this->prophesize(EntityRepository::class); + $repo->findOneBy(['shortCode' => $shortCode])->willReturn($shortUrl); + $this->em->getRepository(ShortUrl::class)->willReturn($repo->reveal())->shouldBeCalledTimes(1); + + $list = [ + new Visit(), + new Visit(), + ]; + $repo2 = $this->prophesize(VisitRepository::class); + $repo2->findVisitsByShortUrl($shortUrl, null)->willReturn($list); + $this->em->getRepository(Visit::class)->willReturn($repo2->reveal())->shouldBeCalledTimes(1); + + $this->assertEquals($list, $this->visitsTracker->info($shortCode)); } } From 9c6420fe2672ef2ad6afeb02b0f1fffe26cb127c Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 30 Jul 2016 23:01:07 +0200 Subject: [PATCH 14/25] Created VisitServiceTest --- module/Core/test/Service/VisitServiceTest.php | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 module/Core/test/Service/VisitServiceTest.php diff --git a/module/Core/test/Service/VisitServiceTest.php b/module/Core/test/Service/VisitServiceTest.php new file mode 100644 index 00000000..2ecf0a45 --- /dev/null +++ b/module/Core/test/Service/VisitServiceTest.php @@ -0,0 +1,49 @@ +em = $this->prophesize(EntityManager::class); + $this->visitService = new VisitService($this->em->reveal()); + } + + /** + * @test + */ + public function saveVisitsPersistsProvidedVisit() + { + $visit = new Visit(); + $this->em->persist($visit)->shouldBeCalledTimes(1); + $this->em->flush()->shouldBeCalledTimes(1); + $this->visitService->saveVisit($visit); + } + + /** + * @test + */ + public function getUnlocatedVisitsFallbacksToRepository() + { + $repo = $this->prophesize(VisitRepository::class); + $repo->findUnlocatedVisits()->shouldBeCalledTimes(1); + $this->em->getRepository(Visit::class)->willReturn($repo->reveal())->shouldBeCalledTimes(1); + $this->visitService->getUnlocatedVisits(); + } +} From 8c446f0f3b06c2632caac17a62d76637fe4dd758 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 30 Jul 2016 23:07:44 +0200 Subject: [PATCH 15/25] Created Core\ConfigProviderTest --- module/Core/test/ConfigProviderTest.php | 32 +++++++++++++++++++++++++ phpunit.xml.dist | 4 ++++ 2 files changed, 36 insertions(+) create mode 100644 module/Core/test/ConfigProviderTest.php diff --git a/module/Core/test/ConfigProviderTest.php b/module/Core/test/ConfigProviderTest.php new file mode 100644 index 00000000..fdd41cad --- /dev/null +++ b/module/Core/test/ConfigProviderTest.php @@ -0,0 +1,32 @@ +configProvider = new ConfigProvider(); + } + + /** + * @test + */ + public function properConfigIsReturned() + { + $config = $this->configProvider->__invoke(); + + $this->assertArrayHasKey('routes', $config); + $this->assertArrayHasKey('services', $config); + $this->assertArrayHasKey('templates', $config); + $this->assertArrayHasKey('translator', $config); + $this->assertArrayHasKey('zend-expressive', $config); + } +} diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 06e1e939..ddd4f42b 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -20,6 +20,10 @@ ./module/Core/src ./module/Rest/src ./module/CLI/src + + + ./module/Core/src/Repository + From 41939b790da5ba8793dd89cc3ead6d83e13637a7 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 30 Jul 2016 23:17:13 +0200 Subject: [PATCH 16/25] Added more Rest module tests --- module/Rest/test/ConfigProviderTest.php | 33 ++++++++ .../ErrorHandler/JsonErrorHandlerTest.php | 79 +++++++++++++++++++ 2 files changed, 112 insertions(+) create mode 100644 module/Rest/test/ConfigProviderTest.php create mode 100644 module/Rest/test/ErrorHandler/JsonErrorHandlerTest.php diff --git a/module/Rest/test/ConfigProviderTest.php b/module/Rest/test/ConfigProviderTest.php new file mode 100644 index 00000000..377c845f --- /dev/null +++ b/module/Rest/test/ConfigProviderTest.php @@ -0,0 +1,33 @@ +configProvider = new ConfigProvider(); + } + + /** + * @test + */ + public function properConfigIsReturned() + { + $config = $this->configProvider->__invoke(); + + $this->assertArrayHasKey('error_handler', $config); + $this->assertArrayHasKey('middleware_pipeline', $config); + $this->assertArrayHasKey('rest', $config); + $this->assertArrayHasKey('routes', $config); + $this->assertArrayHasKey('services', $config); + $this->assertArrayHasKey('translator', $config); + } +} diff --git a/module/Rest/test/ErrorHandler/JsonErrorHandlerTest.php b/module/Rest/test/ErrorHandler/JsonErrorHandlerTest.php new file mode 100644 index 00000000..ea1eee80 --- /dev/null +++ b/module/Rest/test/ErrorHandler/JsonErrorHandlerTest.php @@ -0,0 +1,79 @@ +errorHandler = new JsonErrorHandler(); + } + + /** + * @test + */ + public function noMatchedRouteReturnsNotFoundResponse() + { + $response = $this->errorHandler->__invoke(ServerRequestFactory::fromGlobals(), new Response()); + $this->assertInstanceOf(Response\JsonResponse::class, $response); + $this->assertEquals(404, $response->getStatusCode()); + } + + /** + * @test + */ + public function matchedRouteWithErrorReturnsMethodNotAllowedResponse() + { + $response = $this->errorHandler->__invoke( + ServerRequestFactory::fromGlobals(), + (new Response())->withStatus(405), + 405 + ); + $this->assertInstanceOf(Response\JsonResponse::class, $response); + $this->assertEquals(405, $response->getStatusCode()); + } + + /** + * @test + */ + public function responseWithErrorKeepsStatus() + { + $response = $this->errorHandler->__invoke( + ServerRequestFactory::fromGlobals()->withAttribute( + RouteResult::class, + RouteResult::fromRouteMatch('foo', 'bar', []) + ), + (new Response())->withStatus(401), + 401 + ); + $this->assertInstanceOf(Response\JsonResponse::class, $response); + $this->assertEquals(401, $response->getStatusCode()); + } + + /** + * @test + */ + public function responseWithoutErrorReturnsStatus500() + { + $response = $this->errorHandler->__invoke( + ServerRequestFactory::fromGlobals()->withAttribute( + RouteResult::class, + RouteResult::fromRouteMatch('foo', 'bar', []) + ), + (new Response())->withStatus(200), + 'Some error' + ); + $this->assertInstanceOf(Response\JsonResponse::class, $response); + $this->assertEquals(500, $response->getStatusCode()); + } +} From f904f79c18ca58320d0e784360c1238154e5d9a6 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 30 Jul 2016 23:26:49 +0200 Subject: [PATCH 17/25] Created CheckAuthenticationMiddlewareTest --- .../CheckAuthenticationMiddlewareTest.php | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 module/Rest/test/Middleware/CheckAuthenticationMiddlewareTest.php diff --git a/module/Rest/test/Middleware/CheckAuthenticationMiddlewareTest.php b/module/Rest/test/Middleware/CheckAuthenticationMiddlewareTest.php new file mode 100644 index 00000000..f2d396af --- /dev/null +++ b/module/Rest/test/Middleware/CheckAuthenticationMiddlewareTest.php @@ -0,0 +1,80 @@ +tokenService = $this->prophesize(RestTokenService::class); + $this->middleware = new CheckAuthenticationMiddleware($this->tokenService->reveal(), Translator::factory([])); + } + + /** + * @test + */ + public function someWhitelistedSituationsFallbackToNextMiddleware() + { + $request = ServerRequestFactory::fromGlobals(); + $response = new Response(); + $isCalled = false; + $this->assertFalse($isCalled); + $this->middleware->__invoke($request, $response, function ($req, $resp) use (&$isCalled) { + $isCalled = true; + }); + $this->assertTrue($isCalled); + + $request = ServerRequestFactory::fromGlobals()->withAttribute( + RouteResult::class, + RouteResult::fromRouteFailure(['GET']) + ); + $response = new Response(); + $isCalled = false; + $this->assertFalse($isCalled); + $this->middleware->__invoke($request, $response, function ($req, $resp) use (&$isCalled) { + $isCalled = true; + }); + $this->assertTrue($isCalled); + + $request = ServerRequestFactory::fromGlobals()->withAttribute( + RouteResult::class, + RouteResult::fromRouteMatch('rest-authenticate', 'foo', []) + ); + $response = new Response(); + $isCalled = false; + $this->assertFalse($isCalled); + $this->middleware->__invoke($request, $response, function ($req, $resp) use (&$isCalled) { + $isCalled = true; + }); + $this->assertTrue($isCalled); + + $request = ServerRequestFactory::fromGlobals()->withAttribute( + RouteResult::class, + RouteResult::fromRouteMatch('bar', 'foo', []) + )->withMethod('OPTIONS'); + $response = new Response(); + $isCalled = false; + $this->assertFalse($isCalled); + $this->middleware->__invoke($request, $response, function ($req, $resp) use (&$isCalled) { + $isCalled = true; + }); + $this->assertTrue($isCalled); + } +} From ef6f4fba668c2429b5d471a161f583f8c102cb42 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 31 Jul 2016 13:01:08 +0200 Subject: [PATCH 18/25] Improved CheckAuthenticationMiddlewareTest --- .../CheckAuthenticationMiddlewareTest.php | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/module/Rest/test/Middleware/CheckAuthenticationMiddlewareTest.php b/module/Rest/test/Middleware/CheckAuthenticationMiddlewareTest.php index f2d396af..650d4d2f 100644 --- a/module/Rest/test/Middleware/CheckAuthenticationMiddlewareTest.php +++ b/module/Rest/test/Middleware/CheckAuthenticationMiddlewareTest.php @@ -3,6 +3,7 @@ namespace ShlinkioTest\Shlink\Rest\Middleware; use PHPUnit_Framework_TestCase as TestCase; use Prophecy\Prophecy\ObjectProphecy; +use Shlinkio\Shlink\Core\Entity\RestToken; use Shlinkio\Shlink\Rest\Middleware\CheckAuthenticationMiddleware; use Shlinkio\Shlink\Rest\Service\RestTokenService; use Zend\Diactoros\Response; @@ -77,4 +78,57 @@ class CheckAuthenticationMiddlewareTest extends TestCase }); $this->assertTrue($isCalled); } + + /** + * @test + */ + public function noHeaderReturnsError() + { + $request = ServerRequestFactory::fromGlobals()->withAttribute( + RouteResult::class, + RouteResult::fromRouteMatch('bar', 'foo', []) + ); + $response = $this->middleware->__invoke($request, new Response()); + $this->assertEquals(401, $response->getStatusCode()); + } + + /** + * @test + */ + public function provideAnExpiredTokenReturnsError() + { + $authToken = 'ABC-abc'; + $request = ServerRequestFactory::fromGlobals()->withAttribute( + RouteResult::class, + RouteResult::fromRouteMatch('bar', 'foo', []) + )->withHeader(CheckAuthenticationMiddleware::AUTH_TOKEN_HEADER, $authToken); + $this->tokenService->getByToken($authToken)->willReturn( + (new RestToken())->setExpirationDate((new \DateTime())->sub(new \DateInterval('P1D'))) + )->shouldBeCalledTimes(1); + + $response = $this->middleware->__invoke($request, new Response()); + $this->assertEquals(401, $response->getStatusCode()); + } + + /** + * @test + */ + public function provideCorrectTokenUpdatesExpirationAndFallbacksToNextMiddleware() + { + $authToken = 'ABC-abc'; + $restToken = (new RestToken())->setExpirationDate((new \DateTime())->add(new \DateInterval('P1D'))); + $request = ServerRequestFactory::fromGlobals()->withAttribute( + RouteResult::class, + RouteResult::fromRouteMatch('bar', 'foo', []) + )->withHeader(CheckAuthenticationMiddleware::AUTH_TOKEN_HEADER, $authToken); + $this->tokenService->getByToken($authToken)->willReturn($restToken)->shouldBeCalledTimes(1); + $this->tokenService->updateExpiration($restToken)->shouldBeCalledTimes(1); + + $isCalled = false; + $this->assertFalse($isCalled); + $this->middleware->__invoke($request, new Response(), function ($req, $resp) use (&$isCalled) { + $isCalled = true; + }); + $this->assertTrue($isCalled); + } } From f701e65f7520ae0e1437dd841b8345c0d33125fc Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 31 Jul 2016 13:14:06 +0200 Subject: [PATCH 19/25] Created RestTokenServiceTest --- .../test/Service/RestTokenServiceTest.php | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 module/Rest/test/Service/RestTokenServiceTest.php diff --git a/module/Rest/test/Service/RestTokenServiceTest.php b/module/Rest/test/Service/RestTokenServiceTest.php new file mode 100644 index 00000000..d4487ff1 --- /dev/null +++ b/module/Rest/test/Service/RestTokenServiceTest.php @@ -0,0 +1,93 @@ +em = $this->prophesize(EntityManager::class); + $this->service = new RestTokenService($this->em->reveal(), [ + 'username' => 'foo', + 'password' => 'bar', + ]); + } + + /** + * @test + */ + public function tokenIsCreatedIfCredentialsAreCorrect() + { + $this->em->persist(Argument::type(RestToken::class))->shouldBeCalledTimes(1); + $this->em->flush()->shouldBeCalledTimes(1); + + $token = $this->service->createToken('foo', 'bar'); + $this->assertInstanceOf(RestToken::class, $token); + $this->assertFalse($token->isExpired()); + } + + /** + * @test + * @expectedException \Shlinkio\Shlink\Rest\Exception\AuthenticationException + */ + public function exceptionIsThrownWhileCreatingTokenWithWrongCredentials() + { + $this->service->createToken('foo', 'wrong'); + } + + /** + * @test + */ + public function restTokenIsReturnedFromTokenString() + { + $authToken = 'ABC-abc'; + $theToken = new RestToken(); + $repo = $this->prophesize(EntityRepository::class); + $repo->findOneBy(['token' => $authToken])->willReturn($theToken)->shouldBeCalledTimes(1); + $this->em->getRepository(RestToken::class)->willReturn($repo->reveal())->shouldBeCalledTimes(1); + + $this->assertSame($theToken, $this->service->getByToken($authToken)); + } + + /** + * @test + * @expectedException \Shlinkio\Shlink\Common\Exception\InvalidArgumentException + */ + public function exceptionIsThrownWhenRequestingWrongToken() + { + $authToken = 'ABC-abc'; + $repo = $this->prophesize(EntityRepository::class); + $repo->findOneBy(['token' => $authToken])->willReturn(null)->shouldBeCalledTimes(1); + $this->em->getRepository(RestToken::class)->willReturn($repo->reveal())->shouldBeCalledTimes(1); + + $this->service->getByToken($authToken); + } + + /** + * @test + */ + public function updateExpirationFlushesEntityManager() + { + $token = $this->prophesize(RestToken::class); + $token->updateExpiration()->shouldBeCalledTimes(1); + $this->em->flush()->shouldBeCalledTimes(1); + + $this->service->updateExpiration($token->reveal()); + } +} From 6f7e4f7e7f27712d52a473af46c29cb5ba139482 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 31 Jul 2016 13:18:36 +0200 Subject: [PATCH 20/25] Created RestUtilsTest --- module/Rest/test/Util/RestUtilsTest.php | 40 +++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 module/Rest/test/Util/RestUtilsTest.php diff --git a/module/Rest/test/Util/RestUtilsTest.php b/module/Rest/test/Util/RestUtilsTest.php new file mode 100644 index 00000000..d53b6905 --- /dev/null +++ b/module/Rest/test/Util/RestUtilsTest.php @@ -0,0 +1,40 @@ +assertEquals( + RestUtils::INVALID_SHORTCODE_ERROR, + RestUtils::getRestErrorCodeFromException(new InvalidShortCodeException()) + ); + $this->assertEquals( + RestUtils::INVALID_URL_ERROR, + RestUtils::getRestErrorCodeFromException(new InvalidUrlException()) + ); + $this->assertEquals( + RestUtils::INVALID_ARGUMENT_ERROR, + RestUtils::getRestErrorCodeFromException(new InvalidArgumentException()) + ); + $this->assertEquals( + RestUtils::INVALID_CREDENTIALS_ERROR, + RestUtils::getRestErrorCodeFromException(new AuthenticationException()) + ); + $this->assertEquals( + RestUtils::UNKNOWN_ERROR, + RestUtils::getRestErrorCodeFromException(new WrongIpException()) + ); + } +} From 878518ced705cf2fc1cafb73f49670667b34e97a Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 31 Jul 2016 13:33:55 +0200 Subject: [PATCH 21/25] Created AuthenticateActionTest --- .../test/Action/AuthenticateActionTest.php | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 module/Rest/test/Action/AuthenticateActionTest.php diff --git a/module/Rest/test/Action/AuthenticateActionTest.php b/module/Rest/test/Action/AuthenticateActionTest.php new file mode 100644 index 00000000..d61da421 --- /dev/null +++ b/module/Rest/test/Action/AuthenticateActionTest.php @@ -0,0 +1,75 @@ +tokenService = $this->prophesize(RestTokenService::class); + $this->action = new AuthenticateAction($this->tokenService->reveal(), Translator::factory([])); + } + + /** + * @test + */ + public function notProvidingAuthDataReturnsError() + { + $resp = $this->action->__invoke(ServerRequestFactory::fromGlobals(), new Response()); + $this->assertEquals(400, $resp->getStatusCode()); + } + + /** + * @test + */ + public function properCredentialsReturnTokenInResponse() + { + $this->tokenService->createToken('foo', 'bar')->willReturn( + (new RestToken())->setToken('abc-ABC') + )->shouldBeCalledTimes(1); + + $request = ServerRequestFactory::fromGlobals()->withParsedBody([ + 'username' => 'foo', + 'password' => 'bar', + ]); + $response = $this->action->__invoke($request, new Response()); + $this->assertEquals(200, $response->getStatusCode()); + + $response->getBody()->rewind(); + $this->assertEquals(['token' => 'abc-ABC'], json_decode($response->getBody()->getContents(), true)); + } + + /** + * @test + */ + public function authenticationExceptionsReturnErrorResponse() + { + $this->tokenService->createToken('foo', 'bar')->willThrow(new AuthenticationException()) + ->shouldBeCalledTimes(1); + + $request = ServerRequestFactory::fromGlobals()->withParsedBody([ + 'username' => 'foo', + 'password' => 'bar', + ]); + $response = $this->action->__invoke($request, new Response()); + $this->assertEquals(401, $response->getStatusCode()); + } +} From 04e0a192add9ae5504fca0ec9be2be685a169716 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 31 Jul 2016 15:58:18 +0200 Subject: [PATCH 22/25] Created CreateShortcodeActionTest --- .../test/Action/CreateShortcodeActionTest.php | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 module/Rest/test/Action/CreateShortcodeActionTest.php diff --git a/module/Rest/test/Action/CreateShortcodeActionTest.php b/module/Rest/test/Action/CreateShortcodeActionTest.php new file mode 100644 index 00000000..dcc56bf6 --- /dev/null +++ b/module/Rest/test/Action/CreateShortcodeActionTest.php @@ -0,0 +1,92 @@ +urlShortener = $this->prophesize(UrlShortener::class); + $this->action = new CreateShortcodeAction($this->urlShortener->reveal(), Translator::factory([]), [ + 'schema' => 'http', + 'hostname' => 'foo.com', + ]); + } + + /** + * @test + */ + public function missingLongUrlParamReturnsError() + { + $response = $this->action->__invoke(ServerRequestFactory::fromGlobals(), new Response()); + $this->assertEquals(400, $response->getStatusCode()); + } + + /** + * @test + */ + public function properShortcodeConversionReturnsData() + { + $this->urlShortener->urlToShortCode(Argument::type(Uri::class))->willReturn('abc123') + ->shouldBeCalledTimes(1); + + $request = ServerRequestFactory::fromGlobals()->withParsedBody([ + 'longUrl' => 'http://www.domain.com/foo/bar', + ]); + $response = $this->action->__invoke($request, new Response()); + $this->assertEquals(200, $response->getStatusCode()); + $this->assertTrue(strpos($response->getBody()->getContents(), 'http://foo.com/abc123') > 0); + } + + /** + * @test + */ + public function anInvalidUrlReturnsError() + { + $this->urlShortener->urlToShortCode(Argument::type(Uri::class))->willThrow(InvalidUrlException::class) + ->shouldBeCalledTimes(1); + + $request = ServerRequestFactory::fromGlobals()->withParsedBody([ + 'longUrl' => 'http://www.domain.com/foo/bar', + ]); + $response = $this->action->__invoke($request, new Response()); + $this->assertEquals(400, $response->getStatusCode()); + $this->assertTrue(strpos($response->getBody()->getContents(), RestUtils::INVALID_URL_ERROR) > 0); + } + + /** + * @test + */ + public function aGenericExceptionWillReturnError() + { + $this->urlShortener->urlToShortCode(Argument::type(Uri::class))->willThrow(\Exception::class) + ->shouldBeCalledTimes(1); + + $request = ServerRequestFactory::fromGlobals()->withParsedBody([ + 'longUrl' => 'http://www.domain.com/foo/bar', + ]); + $response = $this->action->__invoke($request, new Response()); + $this->assertEquals(500, $response->getStatusCode()); + $this->assertTrue(strpos($response->getBody()->getContents(), RestUtils::UNKNOWN_ERROR) > 0); + } +} From c6b7515285166700a6c49c076eae3f4371923fd5 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 31 Jul 2016 16:10:16 +0200 Subject: [PATCH 23/25] Created GetVisitsActionTest --- .../Rest/test/Action/GetVisitsActionTest.php | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 module/Rest/test/Action/GetVisitsActionTest.php diff --git a/module/Rest/test/Action/GetVisitsActionTest.php b/module/Rest/test/Action/GetVisitsActionTest.php new file mode 100644 index 00000000..901c549e --- /dev/null +++ b/module/Rest/test/Action/GetVisitsActionTest.php @@ -0,0 +1,99 @@ +visitsTracker = $this->prophesize(VisitsTracker::class); + $this->action = new GetVisitsAction($this->visitsTracker->reveal(), Translator::factory([])); + } + + /** + * @test + */ + public function providingCorrectShortCodeReturnsVisits() + { + $shortCode = 'abc123'; + $this->visitsTracker->info($shortCode, Argument::type(DateRange::class))->willReturn([]) + ->shouldBeCalledTimes(1); + + $response = $this->action->__invoke( + ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode), + new Response() + ); + $this->assertEquals(200, $response->getStatusCode()); + } + + /** + * @test + */ + public function providingInvalidShortCodeReturnsError() + { + $shortCode = 'abc123'; + $this->visitsTracker->info($shortCode, Argument::type(DateRange::class))->willThrow( + InvalidArgumentException::class + )->shouldBeCalledTimes(1); + + $response = $this->action->__invoke( + ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode), + new Response() + ); + $this->assertEquals(400, $response->getStatusCode()); + } + + /** + * @test + */ + public function unexpectedExceptionWillReturnError() + { + $shortCode = 'abc123'; + $this->visitsTracker->info($shortCode, Argument::type(DateRange::class))->willThrow( + \Exception::class + )->shouldBeCalledTimes(1); + + $response = $this->action->__invoke( + ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode), + new Response() + ); + $this->assertEquals(500, $response->getStatusCode()); + } + + /** + * @test + */ + public function datesAreReadFromQuery() + { + $shortCode = 'abc123'; + $this->visitsTracker->info($shortCode, new DateRange(null, new \DateTime('2016-01-01 00:00:00'))) + ->willReturn([]) + ->shouldBeCalledTimes(1); + + $response = $this->action->__invoke( + ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode) + ->withQueryParams(['endDate' => '2016-01-01 00:00:00']), + new Response() + ); + $this->assertEquals(200, $response->getStatusCode()); + } +} From 08f6d2de78389d0fed61c6165bee8a071259ee3f Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 31 Jul 2016 16:16:26 +0200 Subject: [PATCH 24/25] Created ListShortcodesActionTest --- .../test/Action/ListShortcodesActionTest.php | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 module/Rest/test/Action/ListShortcodesActionTest.php diff --git a/module/Rest/test/Action/ListShortcodesActionTest.php b/module/Rest/test/Action/ListShortcodesActionTest.php new file mode 100644 index 00000000..b5ec0c9d --- /dev/null +++ b/module/Rest/test/Action/ListShortcodesActionTest.php @@ -0,0 +1,66 @@ +service = $this->prophesize(ShortUrlService::class); + $this->action = new ListShortcodesAction($this->service->reveal(), Translator::factory([])); + } + + /** + * @test + */ + public function properListReturnsSuccessResponse() + { + $page = 3; + $this->service->listShortUrls($page)->willReturn(new Paginator(new ArrayAdapter())) + ->shouldBeCalledTimes(1); + + $response = $this->action->__invoke( + ServerRequestFactory::fromGlobals()->withQueryParams([ + 'page' => $page, + ]), + new Response() + ); + $this->assertEquals(200, $response->getStatusCode()); + } + + /** + * @test + */ + public function anExceptionsReturnsErrorResponse() + { + $page = 3; + $this->service->listShortUrls($page)->willThrow(\Exception::class) + ->shouldBeCalledTimes(1); + + $response = $this->action->__invoke( + ServerRequestFactory::fromGlobals()->withQueryParams([ + 'page' => $page, + ]), + new Response() + ); + $this->assertEquals(500, $response->getStatusCode()); + } +} From 3d5e5d5df95a1ed3c0cb02d8700f8966f122f10d Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 31 Jul 2016 16:24:00 +0200 Subject: [PATCH 25/25] Created ResolveUrlActionTest --- .../Rest/test/Action/ResolveUrlActionTest.php | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 module/Rest/test/Action/ResolveUrlActionTest.php diff --git a/module/Rest/test/Action/ResolveUrlActionTest.php b/module/Rest/test/Action/ResolveUrlActionTest.php new file mode 100644 index 00000000..0bd3bded --- /dev/null +++ b/module/Rest/test/Action/ResolveUrlActionTest.php @@ -0,0 +1,90 @@ +urlShortener = $this->prophesize(UrlShortener::class); + $this->action = new ResolveUrlAction($this->urlShortener->reveal(), Translator::factory([])); + } + + /** + * @test + */ + public function incorrectShortCodeReturnsError() + { + $shortCode = 'abc123'; + $this->urlShortener->shortCodeToUrl($shortCode)->willReturn(null) + ->shouldBeCalledTimes(1); + + $request = ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode); + $response = $this->action->__invoke($request, new Response()); + $this->assertEquals(400, $response->getStatusCode()); + $this->assertTrue(strpos($response->getBody()->getContents(), RestUtils::INVALID_ARGUMENT_ERROR) > 0); + } + + /** + * @test + */ + public function correctShortCodeReturnsSuccess() + { + $shortCode = 'abc123'; + $this->urlShortener->shortCodeToUrl($shortCode)->willReturn('http://domain.com/foo/bar') + ->shouldBeCalledTimes(1); + + $request = ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode); + $response = $this->action->__invoke($request, new Response()); + $this->assertEquals(200, $response->getStatusCode()); + $this->assertTrue(strpos($response->getBody()->getContents(), 'http://domain.com/foo/bar') > 0); + } + + /** + * @test + */ + public function invalidShortCodeExceptionReturnsError() + { + $shortCode = 'abc123'; + $this->urlShortener->shortCodeToUrl($shortCode)->willThrow(InvalidShortCodeException::class) + ->shouldBeCalledTimes(1); + + $request = ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode); + $response = $this->action->__invoke($request, new Response()); + $this->assertEquals(400, $response->getStatusCode()); + $this->assertTrue(strpos($response->getBody()->getContents(), RestUtils::INVALID_SHORTCODE_ERROR) > 0); + } + + /** + * @test + */ + public function unexpectedExceptionWillReturnError() + { + $shortCode = 'abc123'; + $this->urlShortener->shortCodeToUrl($shortCode)->willThrow(\Exception::class) + ->shouldBeCalledTimes(1); + + $request = ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode); + $response = $this->action->__invoke($request, new Response()); + $this->assertEquals(500, $response->getStatusCode()); + $this->assertTrue(strpos($response->getBody()->getContents(), RestUtils::UNKNOWN_ERROR) > 0); + } +}