diff --git a/config/autoload/errorhandler.local.php.dist b/config/autoload/errorhandler.local.php.dist index d6de56d7..afbc52ed 100644 --- a/config/autoload/errorhandler.local.php.dist +++ b/config/autoload/errorhandler.local.php.dist @@ -1,6 +1,5 @@ InvokableFactory::class, // View - ContentBasedErrorHandler::class => AnnotatedFactory::class, - ErrorHandlerManager::class => ErrorHandlerManagerFactory::class, Template\TemplateRendererInterface::class => Twig\TwigRendererFactory::class, ], 'aliases' => [ 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/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/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 + ); + } +} 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); + } +} 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); + } +} 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); + } +} 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 73% rename from module/Common/src/Expressive/ContentBasedErrorHandler.php rename to module/Common/src/ErrorHandler/ContentBasedErrorHandler.php index df6cde91..be19e848 100644 --- a/module/Common/src/Expressive/ContentBasedErrorHandler.php +++ b/module/Common/src/ErrorHandler/ContentBasedErrorHandler.php @@ -1,5 +1,5 @@ hasHeader('Accept') ? $request->getHeaderLine('Accept') : self::DEFAULT_CONTENT; $accepts = explode(',', $accepts); foreach ($accepts as $accept) { @@ -59,8 +60,17 @@ class ContentBasedErrorHandler implements ErrorHandlerInterface return $this->errorHandlerManager->get($accept); } + // If it wasn't possible to find an error handler for accepted content type, use default one if registered + if ($this->errorHandlerManager->has(self::DEFAULT_CONTENT)) { + return $this->errorHandlerManager->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 )); } } diff --git a/module/Common/src/Expressive/ErrorHandlerInterface.php b/module/Common/src/ErrorHandler/ErrorHandlerInterface.php similarity index 89% rename from module/Common/src/Expressive/ErrorHandlerInterface.php rename to module/Common/src/ErrorHandler/ErrorHandlerInterface.php index 9f0b5803..9676c40a 100644 --- a/module/Common/src/Expressive/ErrorHandlerInterface.php +++ b/module/Common/src/ErrorHandler/ErrorHandlerInterface.php @@ -1,5 +1,5 @@ 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/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'); + } +} 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()); + } +} 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()); + } +} 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); + }); + } +} 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/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(); + } +} 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)); } } diff --git a/module/Rest/config/error-handler.config.php b/module/Rest/config/error-handler.config.php index 4b5b7e75..fef71303 100644 --- a/module/Rest/config/error-handler.config.php +++ b/module/Rest/config/error-handler.config.php @@ -1,5 +1,5 @@ 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()); + } +} 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); + } +} 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()); + } +} 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()); + } +} 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); + } +} 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()); + } +} diff --git a/module/Rest/test/Middleware/CheckAuthenticationMiddlewareTest.php b/module/Rest/test/Middleware/CheckAuthenticationMiddlewareTest.php new file mode 100644 index 00000000..650d4d2f --- /dev/null +++ b/module/Rest/test/Middleware/CheckAuthenticationMiddlewareTest.php @@ -0,0 +1,134 @@ +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); + } + + /** + * @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); + } +} 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()); + } +} 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()) + ); + } +} 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 +