From 7b746f76b063f83d27e2afc8b2d24d57e43f8e4b Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 6 Aug 2016 13:18:27 +0200 Subject: [PATCH] Created APiKeyService and tests --- module/Rest/src/Action/AuthenticateAction.php | 6 +- module/Rest/src/Entity/ApiKey.php | 12 +- module/Rest/src/Service/ApiKeyService.php | 85 +++++++++++ .../src/Service/ApiKeyServiceInterface.php | 31 ++++ .../Rest/test/Service/ApiKeyServiceTest.php | 142 ++++++++++++++++++ 5 files changed, 273 insertions(+), 3 deletions(-) create mode 100644 module/Rest/src/Service/ApiKeyService.php create mode 100644 module/Rest/src/Service/ApiKeyServiceInterface.php create mode 100644 module/Rest/test/Service/ApiKeyServiceTest.php diff --git a/module/Rest/src/Action/AuthenticateAction.php b/module/Rest/src/Action/AuthenticateAction.php index 7d564e4f..37abbc56 100644 --- a/module/Rest/src/Action/AuthenticateAction.php +++ b/module/Rest/src/Action/AuthenticateAction.php @@ -44,10 +44,12 @@ class AuthenticateAction extends AbstractRestAction public function dispatch(Request $request, Response $response, callable $out = null) { $authData = $request->getParsedBody(); - if (! isset($authData['username'], $authData['password'])) { + if (! isset($authData['apiKey'], $authData['username'], $authData['password'])) { return new JsonResponse([ 'error' => RestUtils::INVALID_ARGUMENT_ERROR, - 'message' => $this->translator->translate('You have to provide both "username" and "password"'), + 'message' => $this->translator->translate( + 'You have to provide a valid API key under the "apiKey" param name.' + ), ], 400); } diff --git a/module/Rest/src/Entity/ApiKey.php b/module/Rest/src/Entity/ApiKey.php index 7dc1614f..e0600e88 100644 --- a/module/Rest/src/Entity/ApiKey.php +++ b/module/Rest/src/Entity/ApiKey.php @@ -84,7 +84,7 @@ class ApiKey extends AbstractEntity return false; } - return $this->expirationDate >= new \DateTime(); + return $this->expirationDate < new \DateTime(); } /** @@ -105,6 +105,16 @@ class ApiKey extends AbstractEntity return $this; } + /** + * Disables this API key + * + * @return $this + */ + public function disable() + { + return $this->setEnabled(false); + } + /** * Tells if this api key is enabled and not expired * diff --git a/module/Rest/src/Service/ApiKeyService.php b/module/Rest/src/Service/ApiKeyService.php new file mode 100644 index 00000000..3aa13b62 --- /dev/null +++ b/module/Rest/src/Service/ApiKeyService.php @@ -0,0 +1,85 @@ +em = $em; + } + + /** + * Creates a new ApiKey with provided expiration date + * + * @param \DateTime $expirationDate + * @return ApiKey + */ + public function create(\DateTime $expirationDate = null) + { + $key = new ApiKey(); + if (isset($expirationDate)) { + $key->setExpirationDate($expirationDate); + } + + $this->em->persist($key); + $this->em->flush(); + + return $key; + } + + /** + * Checks if provided key is a valid api key + * + * @param string $key + * @return bool + */ + public function check($key) + { + /** @var ApiKey $apiKey */ + $apiKey = $this->em->getRepository(ApiKey::class)->findOneBy([ + 'key' => $key, + ]); + if (! isset($apiKey)) { + return false; + } + + return $apiKey->isValid(); + } + + /** + * Disables provided api key + * + * @param string $key + * @return ApiKey + */ + public function disable($key) + { + /** @var ApiKey $apiKey */ + $apiKey = $this->em->getRepository(ApiKey::class)->findOneBy([ + 'key' => $key, + ]); + if (! isset($apiKey)) { + throw new InvalidArgumentException(sprintf('API key "%s" does not exist and can\'t be disabled', $key)); + } + + $apiKey->disable(); + $this->em->flush(); + return $apiKey; + } +} diff --git a/module/Rest/src/Service/ApiKeyServiceInterface.php b/module/Rest/src/Service/ApiKeyServiceInterface.php new file mode 100644 index 00000000..0c1f526b --- /dev/null +++ b/module/Rest/src/Service/ApiKeyServiceInterface.php @@ -0,0 +1,31 @@ +em = $this->prophesize(EntityManager::class); + $this->service = new ApiKeyService($this->em->reveal()); + } + + /** + * @test + */ + public function keyIsProperlyCreated() + { + $this->em->flush()->shouldBeCalledTimes(1); + $this->em->persist(Argument::type(ApiKey::class))->shouldBeCalledTimes(1); + + $key = $this->service->create(); + $this->assertNull($key->getExpirationDate()); + } + + /** + * @test + */ + public function keyIsProperlyCreatedWithExpirationDate() + { + $this->em->flush()->shouldBeCalledTimes(1); + $this->em->persist(Argument::type(ApiKey::class))->shouldBeCalledTimes(1); + + $date = new \DateTime('2030-01-01'); + $key = $this->service->create($date); + $this->assertSame($date, $key->getExpirationDate()); + } + + /** + * @test + */ + public function checkReturnsFalseWhenKeyIsInvalid() + { + $repo = $this->prophesize(EntityRepository::class); + $repo->findOneBy(['key' => '12345'])->willReturn(null) + ->shouldBeCalledTimes(1); + $this->em->getRepository(ApiKey::class)->willReturn($repo->reveal()); + + $this->assertFalse($this->service->check('12345')); + } + + /** + * @test + */ + public function checkReturnsFalseWhenKeyIsDisabled() + { + $key = new ApiKey(); + $key->disable(); + $repo = $this->prophesize(EntityRepository::class); + $repo->findOneBy(['key' => '12345'])->willReturn($key) + ->shouldBeCalledTimes(1); + $this->em->getRepository(ApiKey::class)->willReturn($repo->reveal()); + + $this->assertFalse($this->service->check('12345')); + } + + /** + * @test + */ + public function checkReturnsFalseWhenKeyIsExpired() + { + $key = new ApiKey(); + $key->setExpirationDate((new \DateTime())->sub(new \DateInterval('P1D'))); + $repo = $this->prophesize(EntityRepository::class); + $repo->findOneBy(['key' => '12345'])->willReturn($key) + ->shouldBeCalledTimes(1); + $this->em->getRepository(ApiKey::class)->willReturn($repo->reveal()); + + $this->assertFalse($this->service->check('12345')); + } + + /** + * @test + */ + public function checkReturnsTrueWhenConditionsAreFavorable() + { + $repo = $this->prophesize(EntityRepository::class); + $repo->findOneBy(['key' => '12345'])->willReturn(new ApiKey()) + ->shouldBeCalledTimes(1); + $this->em->getRepository(ApiKey::class)->willReturn($repo->reveal()); + + $this->assertTrue($this->service->check('12345')); + } + + /** + * @test + * @expectedException \Shlinkio\Shlink\Common\Exception\InvalidArgumentException + */ + public function disableThrowsExceptionWhenNoTokenIsFound() + { + $repo = $this->prophesize(EntityRepository::class); + $repo->findOneBy(['key' => '12345'])->willReturn(null) + ->shouldBeCalledTimes(1); + $this->em->getRepository(ApiKey::class)->willReturn($repo->reveal()); + + $this->service->disable('12345'); + } + + /** + * @test + */ + public function disableReturnsDisabledKeyWhenFOund() + { + $key = new ApiKey(); + $repo = $this->prophesize(EntityRepository::class); + $repo->findOneBy(['key' => '12345'])->willReturn($key) + ->shouldBeCalledTimes(1); + $this->em->getRepository(ApiKey::class)->willReturn($repo->reveal()); + + $this->em->flush()->shouldBeCalledTimes(1); + + $this->assertTrue($key->isEnabled()); + $returnedKey = $this->service->disable('12345'); + $this->assertFalse($key->isEnabled()); + $this->assertSame($key, $returnedKey); + } +}