From c9ff2b383497086dd5dc8d83504227bcdc8d2514 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 10 Jan 2021 20:05:14 +0100 Subject: [PATCH] Updated services required to initialize API keys with roles --- composer.json | 2 +- module/Core/src/Domain/DomainService.php | 15 ++++++++++ .../src/Domain/DomainServiceInterface.php | 6 ++++ module/Core/test/Domain/DomainServiceTest.php | 30 +++++++++++++++++++ module/Rest/src/Service/ApiKeyService.php | 9 ++++-- .../src/Service/ApiKeyServiceInterface.php | 3 +- .../Rest/test/Service/ApiKeyServiceTest.php | 14 ++++++--- 7 files changed, 70 insertions(+), 9 deletions(-) diff --git a/composer.json b/composer.json index 88919e4a..5fd37265 100644 --- a/composer.json +++ b/composer.json @@ -30,7 +30,7 @@ "laminas/laminas-diactoros": "^2.1.3", "laminas/laminas-inputfilter": "^2.10", "laminas/laminas-paginator": "^2.8", - "laminas/laminas-servicemanager": "^3.4", + "laminas/laminas-servicemanager": "^3.6", "laminas/laminas-stdlib": "^3.2", "lcobucci/jwt": "^4.0", "league/uri": "^6.2", diff --git a/module/Core/src/Domain/DomainService.php b/module/Core/src/Domain/DomainService.php index e80f36b7..5a573799 100644 --- a/module/Core/src/Domain/DomainService.php +++ b/module/Core/src/Domain/DomainService.php @@ -45,6 +45,9 @@ class DomainService implements DomainServiceInterface ]; } + /** + * @throws DomainNotFoundException + */ public function getDomain(string $domainId): Domain { /** @var Domain|null $domain */ @@ -55,4 +58,16 @@ class DomainService implements DomainServiceInterface return $domain; } + + public function getOrCreate(string $authority): Domain + { + $repo = $this->em->getRepository(Domain::class); + /** @var Domain|null $domain */ + $domain = $repo->findOneBy(['authority' => $authority]) ?? new Domain($authority); + + $this->em->persist($domain); + $this->em->flush(); + + return $domain; + } } diff --git a/module/Core/src/Domain/DomainServiceInterface.php b/module/Core/src/Domain/DomainServiceInterface.php index 0a2ef914..3588fbc6 100644 --- a/module/Core/src/Domain/DomainServiceInterface.php +++ b/module/Core/src/Domain/DomainServiceInterface.php @@ -6,6 +6,7 @@ namespace Shlinkio\Shlink\Core\Domain; use Shlinkio\Shlink\Core\Domain\Model\DomainItem; use Shlinkio\Shlink\Core\Entity\Domain; +use Shlinkio\Shlink\Core\Exception\DomainNotFoundException; use Shlinkio\Shlink\Rest\Entity\ApiKey; interface DomainServiceInterface @@ -15,5 +16,10 @@ interface DomainServiceInterface */ public function listDomains(?ApiKey $apiKey = null): array; + /** + * @throws DomainNotFoundException + */ public function getDomain(string $domainId): Domain; + + public function getOrCreate(string $authority): Domain; } diff --git a/module/Core/test/Domain/DomainServiceTest.php b/module/Core/test/Domain/DomainServiceTest.php index 7c21014c..6a1ccef8 100644 --- a/module/Core/test/Domain/DomainServiceTest.php +++ b/module/Core/test/Domain/DomainServiceTest.php @@ -6,6 +6,7 @@ namespace ShlinkioTest\Shlink\Core\Domain; use Doctrine\ORM\EntityManagerInterface; use PHPUnit\Framework\TestCase; +use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; use Prophecy\Prophecy\ObjectProphecy; use Shlinkio\Shlink\Core\Domain\DomainService; @@ -111,4 +112,33 @@ class DomainServiceTest extends TestCase self::assertSame($domain, $result); $find->shouldHaveBeenCalledOnce(); } + + /** + * @test + * @dataProvider provideFoundDomains + */ + public function getOrCreateAlwaysPersistsDomain(?Domain $foundDomain): void + { + $authority = 'example.com'; + $repo = $this->prophesize(DomainRepositoryInterface::class); + $repo->findOneBy(['authority' => $authority])->willReturn($foundDomain); + $getRepo = $this->em->getRepository(Domain::class)->willReturn($repo->reveal()); + $persist = $this->em->persist($foundDomain !== null ? $foundDomain : Argument::type(Domain::class)); + $flush = $this->em->flush(); + + $result = $this->domainService->getOrCreate($authority); + + if ($foundDomain !== null) { + self::assertSame($result, $foundDomain); + } + $getRepo->shouldHaveBeenCalledOnce(); + $persist->shouldHaveBeenCalledOnce(); + $flush->shouldHaveBeenCalledOnce(); + } + + public function provideFoundDomains(): iterable + { + yield 'domain not found' => [null]; + yield 'domain found' => [new Domain('')]; + } } diff --git a/module/Rest/src/Service/ApiKeyService.php b/module/Rest/src/Service/ApiKeyService.php index 6fb61be9..917cf048 100644 --- a/module/Rest/src/Service/ApiKeyService.php +++ b/module/Rest/src/Service/ApiKeyService.php @@ -7,6 +7,7 @@ namespace Shlinkio\Shlink\Rest\Service; use Cake\Chronos\Chronos; use Doctrine\ORM\EntityManagerInterface; use Shlinkio\Shlink\Common\Exception\InvalidArgumentException; +use Shlinkio\Shlink\Rest\ApiKey\Model\RoleDefinition; use Shlinkio\Shlink\Rest\Entity\ApiKey; use function sprintf; @@ -20,9 +21,13 @@ class ApiKeyService implements ApiKeyServiceInterface $this->em = $em; } - public function create(?Chronos $expirationDate = null): ApiKey + public function create(?Chronos $expirationDate = null, RoleDefinition ...$roleDefinitions): ApiKey { $key = new ApiKey($expirationDate); + foreach ($roleDefinitions as $definition) { + $key->registerRole($definition); + } + $this->em->persist($key); $this->em->flush(); @@ -31,7 +36,6 @@ class ApiKeyService implements ApiKeyServiceInterface public function check(string $key): ApiKeyCheckResult { - /** @var ApiKey|null $apiKey */ $apiKey = $this->getByKey($key); return new ApiKeyCheckResult($apiKey); } @@ -41,7 +45,6 @@ class ApiKeyService implements ApiKeyServiceInterface */ public function disable(string $key): ApiKey { - /** @var ApiKey|null $apiKey */ $apiKey = $this->getByKey($key); if ($apiKey === null) { throw new InvalidArgumentException(sprintf('API key "%s" does not exist and can\'t be disabled', $key)); diff --git a/module/Rest/src/Service/ApiKeyServiceInterface.php b/module/Rest/src/Service/ApiKeyServiceInterface.php index e8c6d0ea..562f106b 100644 --- a/module/Rest/src/Service/ApiKeyServiceInterface.php +++ b/module/Rest/src/Service/ApiKeyServiceInterface.php @@ -6,11 +6,12 @@ namespace Shlinkio\Shlink\Rest\Service; use Cake\Chronos\Chronos; use Shlinkio\Shlink\Common\Exception\InvalidArgumentException; +use Shlinkio\Shlink\Rest\ApiKey\Model\RoleDefinition; use Shlinkio\Shlink\Rest\Entity\ApiKey; interface ApiKeyServiceInterface { - public function create(?Chronos $expirationDate = null): ApiKey; + public function create(?Chronos $expirationDate = null, RoleDefinition ...$roleDefinitions): ApiKey; public function check(string $key): ApiKeyCheckResult; diff --git a/module/Rest/test/Service/ApiKeyServiceTest.php b/module/Rest/test/Service/ApiKeyServiceTest.php index 6d228661..3bbdbf64 100644 --- a/module/Rest/test/Service/ApiKeyServiceTest.php +++ b/module/Rest/test/Service/ApiKeyServiceTest.php @@ -12,6 +12,7 @@ use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; use Prophecy\Prophecy\ObjectProphecy; use Shlinkio\Shlink\Common\Exception\InvalidArgumentException; +use Shlinkio\Shlink\Rest\ApiKey\Model\RoleDefinition; use Shlinkio\Shlink\Rest\Entity\ApiKey; use Shlinkio\Shlink\Rest\Service\ApiKeyService; @@ -31,21 +32,26 @@ class ApiKeyServiceTest extends TestCase /** * @test * @dataProvider provideCreationDate + * @param RoleDefinition[] $roles */ - public function apiKeyIsProperlyCreated(?Chronos $date): void + public function apiKeyIsProperlyCreated(?Chronos $date, array $roles): void { $this->em->flush()->shouldBeCalledOnce(); $this->em->persist(Argument::type(ApiKey::class))->shouldBeCalledOnce(); - $key = $this->service->create($date); + $key = $this->service->create($date, ...$roles); self::assertEquals($date, $key->getExpirationDate()); + foreach ($roles as $roleDefinition) { + self::assertTrue($key->hasRole($roleDefinition->roleName())); + } } public function provideCreationDate(): iterable { - yield 'no expiration date' => [null]; - yield 'expiration date' => [Chronos::parse('2030-01-01')]; + yield 'no expiration date' => [null, []]; + yield 'expiration date' => [Chronos::parse('2030-01-01'), []]; + yield 'roles' => [null, [RoleDefinition::forDomain('123'), RoleDefinition::forAuthoredShortUrls()]]; } /**