Merge pull request #1541 from acelaya-forks/feature/initial-api-key

Feature/initial api key
This commit is contained in:
Alejandro Celaya 2022-09-11 13:23:44 +02:00 committed by GitHub
commit a87f6c6709
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
88 changed files with 303 additions and 84 deletions

View file

@ -30,6 +30,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this
Non-error responses are not affected.
* [#1513](https://github.com/shlinkio/shlink/issues/1513) Added publishing of the docker image in GHCR.
* [#1114](https://github.com/shlinkio/shlink/issues/1114) Added support to provide an initial API key via `INITIAL_API_KEY` env var, when running Shlink with openswoole or RoadRunner.
Also, the installer tool now allows to generate an initial API key that can be copy-pasted (this tool is run interactively), in case you use php-fpm or you don't want to use env vars.
### Changed
* [#1339](https://github.com/shlinkio/shlink/issues/1339) Added new test suite for CLI E2E tests.

View file

@ -47,7 +47,7 @@
"shlinkio/shlink-config": "dev-main#33004e6 as 2.1",
"shlinkio/shlink-event-dispatcher": "dev-main#48c0137 as 2.6",
"shlinkio/shlink-importer": "^4.0",
"shlinkio/shlink-installer": "dev-develop#f1cc5c7 as 8.2",
"shlinkio/shlink-installer": "dev-develop#a01bca9 as 8.2",
"shlinkio/shlink-ip-geolocation": "^3.0",
"spiral/roadrunner": "^2.11",
"spiral/roadrunner-jobs": "^2.3",
@ -92,6 +92,7 @@
"ShlinkioCliTest\\Shlink\\CLI\\": "module/CLI/test-cli",
"ShlinkioTest\\Shlink\\Rest\\": "module/Rest/test",
"ShlinkioApiTest\\Shlink\\Rest\\": "module/Rest/test-api",
"ShlinkioDbTest\\Shlink\\Rest\\": "module/Rest/test-db",
"ShlinkioTest\\Shlink\\Core\\": "module/Core/test",
"ShlinkioDbTest\\Shlink\\Core\\": "module/Core/test-db"
},

View file

@ -8,8 +8,8 @@ return [
'debug' => false,
// Disabling config cache for cli, ensures it's never used for openswoole and also that console commands don't
// generate a cache file that's then used by non-openswoole web executions
// Disabling config cache for cli, ensures it's never used for openswoole/RoadRunner, and also that console
// commands don't generate a cache file that's then used by php-fpm web executions
ConfigAggregator::ENABLE_CACHE => PHP_SAPI !== 'cli',
];

View file

@ -82,6 +82,9 @@ return [
InstallationCommand::GEOLITE_DOWNLOAD_DB->value => [
'command' => 'bin/cli ' . Command\Visit\DownloadGeoLiteDbCommand::NAME,
],
InstallationCommand::API_KEY_GENERATE->value => [
'command' => 'bin/cli ' . Command\Api\GenerateKeyCommand::NAME,
],
],
],

View file

@ -19,7 +19,7 @@ class DisableKeyCommandTest extends TestCase
private CommandTester $commandTester;
private ObjectProphecy $apiKeyService;
public function setUp(): void
protected function setUp(): void
{
$this->apiKeyService = $this->prophesize(ApiKeyServiceInterface::class);
$this->commandTester = $this->testerForCommand(new DisableKeyCommand($this->apiKeyService->reveal()));

View file

@ -23,7 +23,7 @@ class GenerateKeyCommandTest extends TestCase
private CommandTester $commandTester;
private ObjectProphecy $apiKeyService;
public function setUp(): void
protected function setUp(): void
{
$this->apiKeyService = $this->prophesize(ApiKeyServiceInterface::class);
$roleResolver = $this->prophesize(RoleResolverInterface::class);

View file

@ -23,7 +23,7 @@ class ListKeysCommandTest extends TestCase
private CommandTester $commandTester;
private ObjectProphecy $apiKeyService;
public function setUp(): void
protected function setUp(): void
{
$this->apiKeyService = $this->prophesize(ApiKeyServiceInterface::class);
$this->commandTester = $this->testerForCommand(new ListKeysCommand($this->apiKeyService->reveal()));

View file

@ -33,7 +33,7 @@ class CreateDatabaseCommandTest extends TestCase
private ObjectProphecy $schemaManager;
private ObjectProphecy $driver;
public function setUp(): void
protected function setUp(): void
{
$locker = $this->prophesize(LockFactory::class);
$lock = $this->prophesize(LockInterface::class);

View file

@ -23,7 +23,7 @@ class MigrateDatabaseCommandTest extends TestCase
private CommandTester $commandTester;
private ObjectProphecy $processHelper;
public function setUp(): void
protected function setUp(): void
{
$locker = $this->prophesize(LockFactory::class);
$lock = $this->prophesize(LockInterface::class);

View file

@ -24,7 +24,7 @@ class DomainRedirectsCommandTest extends TestCase
private CommandTester $commandTester;
private ObjectProphecy $domainService;
public function setUp(): void
protected function setUp(): void
{
$this->domainService = $this->prophesize(DomainServiceInterface::class);
$this->commandTester = $this->testerForCommand(new DomainRedirectsCommand($this->domainService->reveal()));

View file

@ -23,7 +23,7 @@ class ListDomainsCommandTest extends TestCase
private CommandTester $commandTester;
private ObjectProphecy $domainService;
public function setUp(): void
protected function setUp(): void
{
$this->domainService = $this->prophesize(DomainServiceInterface::class);
$this->commandTester = $this->testerForCommand(new ListDomainsCommand($this->domainService->reveal()));

View file

@ -30,7 +30,7 @@ class CreateShortUrlCommandTest extends TestCase
private ObjectProphecy $urlShortener;
private ObjectProphecy $stringifier;
public function setUp(): void
protected function setUp(): void
{
$this->urlShortener = $this->prophesize(UrlShortener::class);
$this->stringifier = $this->prophesize(ShortUrlStringifierInterface::class);

View file

@ -26,7 +26,7 @@ class DeleteShortUrlCommandTest extends TestCase
private CommandTester $commandTester;
private ObjectProphecy $service;
public function setUp(): void
protected function setUp(): void
{
$this->service = $this->prophesize(DeleteShortUrlServiceInterface::class);
$this->commandTester = $this->testerForCommand(new DeleteShortUrlCommand($this->service->reveal()));

View file

@ -33,7 +33,7 @@ class GetShortUrlVisitsCommandTest extends TestCase
private CommandTester $commandTester;
private ObjectProphecy $visitsHelper;
public function setUp(): void
protected function setUp(): void
{
$this->visitsHelper = $this->prophesize(VisitsStatsHelperInterface::class);
$command = new GetShortUrlVisitsCommand($this->visitsHelper->reveal());

View file

@ -33,7 +33,7 @@ class ListShortUrlsCommandTest extends TestCase
private CommandTester $commandTester;
private ObjectProphecy $shortUrlService;
public function setUp(): void
protected function setUp(): void
{
$this->shortUrlService = $this->prophesize(ShortUrlServiceInterface::class);
$command = new ListShortUrlsCommand($this->shortUrlService->reveal(), new ShortUrlDataTransformer(

View file

@ -25,7 +25,7 @@ class ResolveUrlCommandTest extends TestCase
private CommandTester $commandTester;
private ObjectProphecy $urlResolver;
public function setUp(): void
protected function setUp(): void
{
$this->urlResolver = $this->prophesize(ShortUrlResolverInterface::class);
$this->commandTester = $this->testerForCommand(new ResolveUrlCommand($this->urlResolver->reveal()));

View file

@ -18,7 +18,7 @@ class DeleteTagsCommandTest extends TestCase
private CommandTester $commandTester;
private ObjectProphecy $tagService;
public function setUp(): void
protected function setUp(): void
{
$this->tagService = $this->prophesize(TagServiceInterface::class);
$this->commandTester = $this->testerForCommand(new DeleteTagsCommand($this->tagService->reveal()));

View file

@ -22,7 +22,7 @@ class ListTagsCommandTest extends TestCase
private CommandTester $commandTester;
private ObjectProphecy $tagService;
public function setUp(): void
protected function setUp(): void
{
$this->tagService = $this->prophesize(TagServiceInterface::class);
$this->commandTester = $this->testerForCommand(new ListTagsCommand($this->tagService->reveal()));

View file

@ -21,7 +21,7 @@ class RenameTagCommandTest extends TestCase
private CommandTester $commandTester;
private ObjectProphecy $tagService;
public function setUp(): void
protected function setUp(): void
{
$this->tagService = $this->prophesize(TagServiceInterface::class);
$this->commandTester = $this->testerForCommand(new RenameTagCommand($this->tagService->reveal()));

View file

@ -40,7 +40,7 @@ class LocateVisitsCommandTest extends TestCase
private ObjectProphecy $lock;
private ObjectProphecy $downloadDbCommand;
public function setUp(): void
protected function setUp(): void
{
$this->visitService = $this->prophesize(VisitLocator::class);
$this->ipResolver = $this->prophesize(IpLocationResolverInterface::class);

View file

@ -12,7 +12,7 @@ class ConfigProviderTest extends TestCase
{
private ConfigProvider $configProvider;
public function setUp(): void
protected function setUp(): void
{
$this->configProvider = new ConfigProvider();
}

View file

@ -16,7 +16,7 @@ class ApplicationFactoryTest extends TestCase
private ApplicationFactory $factory;
public function setUp(): void
protected function setUp(): void
{
$this->factory = new ApplicationFactory();
}

View file

@ -32,7 +32,7 @@ class GeolocationDbUpdaterTest extends TestCase
private TrackingOptions $trackingOptions;
private ObjectProphecy $lock;
public function setUp(): void
protected function setUp(): void
{
$this->dbUpdater = $this->prophesize(DbUpdaterInterface::class);
$this->geoLiteDbReader = $this->prophesize(Reader::class);

View file

@ -21,7 +21,7 @@ class ShlinkTableTest extends TestCase
private ShlinkTable $shlinkTable;
private ObjectProphecy $baseTable;
public function setUp(): void
protected function setUp(): void
{
$this->baseTable = $this->prophesize(Table::class);
$this->shlinkTable = ShlinkTable::fromBaseTable($this->baseTable->reveal());

View file

@ -47,6 +47,7 @@ enum EnvVars: string
case PORT = 'PORT';
case TASK_WORKER_NUM = 'TASK_WORKER_NUM';
case WEB_WORKER_NUM = 'WEB_WORKER_NUM';
case INITIAL_API_KEY = 'INITIAL_API_KEY';
case ANONYMIZE_REMOTE_ADDR = 'ANONYMIZE_REMOTE_ADDR';
case TRACK_ORPHAN_VISITS = 'TRACK_ORPHAN_VISITS';
case DISABLE_TRACK_PARAM = 'DISABLE_TRACK_PARAM';

View file

@ -110,7 +110,7 @@ class TagRepository extends EntitySpecificationRepository implements TagReposito
return map(
$this->getEntityManager()->createNativeQuery($nativeQb->getSQL(), $rsm)->getResult(),
static fn (array $row) => new TagInfo($row['tag'], (int) $row['shortUrlsCount'], (int) $row['visitsCount']),
TagInfo::fromRawData(...),
);
}

View file

@ -15,6 +15,11 @@ final class TagInfo implements JsonSerializable
) {
}
public static function fromRawData(array $data): self
{
return new self($data['tag'], (int) $data['shortUrlsCount'], (int) $data['visitsCount']);
}
public function jsonSerialize(): array
{
return [

View file

@ -14,6 +14,7 @@ final class TagsParams extends AbstractInfinitePaginableListParams
private function __construct(
public readonly ?string $searchTerm,
public readonly Ordering $orderBy,
/** @deprecated */
public readonly bool $withStats,
?int $page,
?int $itemsPerPage,

View file

@ -25,7 +25,7 @@ class PixelActionTest extends TestCase
private ObjectProphecy $urlResolver;
private ObjectProphecy $requestTracker;
public function setUp(): void
protected function setUp(): void
{
$this->urlResolver = $this->prophesize(ShortUrlResolverInterface::class);
$this->requestTracker = $this->prophesize(RequestTrackerInterface::class);

View file

@ -39,7 +39,7 @@ class QrCodeActionTest extends TestCase
private ObjectProphecy $urlResolver;
private QrCodeOptions $options;
public function setUp(): void
protected function setUp(): void
{
$router = $this->prophesize(RouterInterface::class);
$router->generateUri(Argument::cetera())->willReturn('/foo/bar');

View file

@ -31,7 +31,7 @@ class RedirectActionTest extends TestCase
private ObjectProphecy $requestTracker;
private ObjectProphecy $redirectRespHelper;
public function setUp(): void
protected function setUp(): void
{
$this->urlResolver = $this->prophesize(ShortUrlResolverInterface::class);
$this->requestTracker = $this->prophesize(RequestTrackerInterface::class);

View file

@ -11,7 +11,7 @@ class BasePathPrefixerTest extends TestCase
{
private BasePathPrefixer $prefixer;
public function setUp(): void
protected function setUp(): void
{
$this->prefixer = new BasePathPrefixer();
}

View file

@ -12,7 +12,7 @@ class ConfigProviderTest extends TestCase
{
private ConfigProvider $configProvider;
public function setUp(): void
protected function setUp(): void
{
$this->configProvider = new ConfigProvider();
}

View file

@ -27,7 +27,7 @@ class DomainServiceTest extends TestCase
private DomainService $domainService;
private ObjectProphecy $em;
public function setUp(): void
protected function setUp(): void
{
$this->em = $this->prophesize(EntityManagerInterface::class);
$this->domainService = new DomainService($this->em->reveal(), 'default.com');

View file

@ -31,7 +31,7 @@ class NotFoundRedirectHandlerTest extends TestCase
private ObjectProphecy $next;
private ServerRequestInterface $req;
public function setUp(): void
protected function setUp(): void
{
$this->redirectOptions = new NotFoundRedirectOptions();
$this->resolver = $this->prophesize(NotFoundRedirectResolverInterface::class);

View file

@ -21,7 +21,7 @@ class NotFoundTemplateHandlerTest extends TestCase
private NotFoundTemplateHandler $handler;
private bool $readFileCalled;
public function setUp(): void
protected function setUp(): void
{
$this->readFileCalled = false;
$readFile = function (string $fileName): string {

View file

@ -18,7 +18,7 @@ class CloseDbConnectionEventListenerDelegatorTest extends TestCase
private CloseDbConnectionEventListenerDelegator $delegator;
private ObjectProphecy $container;
public function setUp(): void
protected function setUp(): void
{
$this->container = $this->prophesize(ContainerInterface::class);
$this->delegator = new CloseDbConnectionEventListenerDelegator();

View file

@ -20,7 +20,7 @@ class CloseDbConnectionEventListenerTest extends TestCase
private ObjectProphecy $em;
public function setUp(): void
protected function setUp(): void
{
$this->em = $this->prophesize(ReopeningEntityManagerInterface::class);
}

View file

@ -36,7 +36,7 @@ class LocateVisitTest extends TestCase
private ObjectProphecy $dbUpdater;
private ObjectProphecy $eventDispatcher;
public function setUp(): void
protected function setUp(): void
{
$this->ipLocationResolver = $this->prophesize(IpLocationResolverInterface::class);
$this->em = $this->prophesize(EntityManagerInterface::class);

View file

@ -31,7 +31,7 @@ class NotifyVisitToMercureTest extends TestCase
private ObjectProphecy $em;
private ObjectProphecy $logger;
public function setUp(): void
protected function setUp(): void
{
$this->helper = $this->prophesize(PublishingHelperInterface::class);
$this->updatesGenerator = $this->prophesize(PublishingUpdatesGeneratorInterface::class);

View file

@ -38,7 +38,7 @@ class NotifyVisitToWebHooksTest extends TestCase
private ObjectProphecy $em;
private ObjectProphecy $logger;
public function setUp(): void
protected function setUp(): void
{
$this->httpClient = $this->prophesize(ClientInterface::class);
$this->em = $this->prophesize(EntityManagerInterface::class);

View file

@ -21,7 +21,7 @@ class PublishingUpdatesGeneratorTest extends TestCase
{
private PublishingUpdatesGenerator $generator;
public function setUp(): void
protected function setUp(): void
{
$this->generator = new PublishingUpdatesGenerator(
new ShortUrlDataTransformer(new ShortUrlStringifier([])),

View file

@ -31,7 +31,7 @@ class DeleteShortUrlServiceTest extends TestCase
private ObjectProphecy $urlResolver;
private string $shortCode;
public function setUp(): void
protected function setUp(): void
{
$shortUrl = ShortUrl::createEmpty()->setVisits(new ArrayCollection(
map(range(0, 10), fn () => Visit::forValidShortUrl(ShortUrl::createEmpty(), Visitor::emptyInstance())),

View file

@ -32,7 +32,7 @@ class ShortUrlResolverTest extends TestCase
private ShortUrlResolver $urlResolver;
private ObjectProphecy $em;
public function setUp(): void
protected function setUp(): void
{
$this->em = $this->prophesize(EntityManagerInterface::class);
$this->urlResolver = new ShortUrlResolver($this->em->reveal());

View file

@ -34,7 +34,7 @@ class ShortUrlServiceTest extends TestCase
private ObjectProphecy $urlResolver;
private ObjectProphecy $titleResolutionHelper;
public function setUp(): void
protected function setUp(): void
{
$this->em = $this->prophesize(EntityManagerInterface::class);
$this->em->persist(Argument::any())->willReturn(null);

View file

@ -30,7 +30,7 @@ class UrlShortenerTest extends TestCase
private ObjectProphecy $shortCodeHelper;
private ObjectProphecy $eventDispatcher;
public function setUp(): void
protected function setUp(): void
{
$this->titleResolutionHelper = $this->prophesize(ShortUrlTitleResolutionHelperInterface::class);
$this->titleResolutionHelper->processTitleAndValidateUrl(Argument::cetera())->willReturnArgument();

View file

@ -22,7 +22,7 @@ class ShortUrlRepositoryAdapterTest extends TestCase
private ObjectProphecy $repo;
public function setUp(): void
protected function setUp(): void
{
$this->repo = $this->prophesize(ShortUrlRepositoryInterface::class);
}

View file

@ -25,7 +25,7 @@ class PersistenceShortUrlRelationResolverTest extends TestCase
private PersistenceShortUrlRelationResolver $resolver;
private ObjectProphecy $em;
public function setUp(): void
protected function setUp(): void
{
$this->em = $this->prophesize(EntityManagerInterface::class);
$this->em->getEventManager()->willReturn(new EventManager());

View file

@ -13,7 +13,7 @@ class SimpleShortUrlRelationResolverTest extends TestCase
{
private SimpleShortUrlRelationResolver $resolver;
public function setUp(): void
protected function setUp(): void
{
$this->resolver = new SimpleShortUrlRelationResolver();
}

View file

@ -17,7 +17,7 @@ class ShortUrlDataTransformerTest extends TestCase
{
private ShortUrlDataTransformer $transformer;
public function setUp(): void
protected function setUp(): void
{
$this->transformer = new ShortUrlDataTransformer(new ShortUrlStringifier([]));
}

View file

@ -33,7 +33,7 @@ class TagServiceTest extends TestCase
private ObjectProphecy $em;
private ObjectProphecy $repo;
public function setUp(): void
protected function setUp(): void
{
$this->em = $this->prophesize(EntityManagerInterface::class);
$this->repo = $this->prophesize(TagRepository::class);

View file

@ -27,7 +27,7 @@ class UrlValidatorTest extends TestCase
private ObjectProphecy $httpClient;
private UrlShortenerOptions $options;
public function setUp(): void
protected function setUp(): void
{
$this->httpClient = $this->prophesize(ClientInterface::class);
$this->options = new UrlShortenerOptions(['validate_url' => true]);

View file

@ -38,7 +38,7 @@ class VisitLocatorTest extends TestCase
private ObjectProphecy $em;
private ObjectProphecy $repo;
public function setUp(): void
protected function setUp(): void
{
$this->em = $this->prophesize(EntityManager::class);
$this->repo = $this->prophesize(VisitRepositoryInterface::class);

View file

@ -43,7 +43,7 @@ class VisitsStatsHelperTest extends TestCase
private VisitsStatsHelper $helper;
private ObjectProphecy $em;
public function setUp(): void
protected function setUp(): void
{
$this->em = $this->prophesize(EntityManagerInterface::class);
$this->helper = new VisitsStatsHelper($this->em->reveal());

View file

@ -26,7 +26,7 @@ class VisitsTrackerTest extends TestCase
private ObjectProphecy $eventDispatcher;
private TrackingOptions $options;
public function setUp(): void
protected function setUp(): void
{
$this->em = $this->prophesize(EntityManager::class);

View file

@ -15,7 +15,8 @@ use function Shlinkio\Shlink\Core\determineTableName;
return static function (ClassMetadata $metadata, array $emConfig): void {
$builder = new ClassMetadataBuilder($metadata);
$builder->setTable(determineTableName('api_keys', $emConfig));
$builder->setTable(determineTableName('api_keys', $emConfig))
->setCustomRepositoryClass(ApiKey\Repository\ApiKeyRepository::class);
$builder->createField('id', Types::BIGINT)
->makePrimaryKey()

View file

@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Rest;
use Mezzio\Application;
use Shlinkio\Shlink\Core\Config\EnvVars;
use const PHP_SAPI;
return [
// We will try to load the initial API key only for openswoole and RoadRunner.
// For php-fpm, the check against the database would happen on every request, resulting in a very bad performance.
'initial_api_key' => PHP_SAPI !== 'cli' ? null : EnvVars::INITIAL_API_KEY->loadFromEnv(),
'dependencies' => [
'delegators' => [
Application::class => [
ApiKey\InitialApiKeyDelegator::class,
],
],
],
];

View file

@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Rest\ApiKey;
use Doctrine\ORM\EntityManager;
use Mezzio\Application;
use Psr\Container\ContainerInterface;
use Shlinkio\Shlink\Rest\ApiKey\Repository\ApiKeyRepositoryInterface;
use Shlinkio\Shlink\Rest\Entity\ApiKey;
class InitialApiKeyDelegator
{
public function __invoke(ContainerInterface $container, string $serviceName, callable $callback): Application
{
$initialApiKey = $container->get('config')['initial_api_key'] ?? null;
if ($initialApiKey !== null) {
$this->createInitialApiKey($initialApiKey, $container);
}
return $callback();
}
private function createInitialApiKey(string $initialApiKey, ContainerInterface $container): void
{
/** @var ApiKeyRepositoryInterface $repo */
$repo = $container->get(EntityManager::class)->getRepository(ApiKey::class);
$repo->createInitialApiKey($initialApiKey);
}
}

View file

@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Rest\ApiKey\Repository;
use Doctrine\DBAL\LockMode;
use Happyr\DoctrineSpecification\Repository\EntitySpecificationRepository;
use Shlinkio\Shlink\Rest\Entity\ApiKey;
class ApiKeyRepository extends EntitySpecificationRepository implements ApiKeyRepositoryInterface
{
public function createInitialApiKey(string $apiKey): void
{
$em = $this->getEntityManager();
$em->wrapInTransaction(function () use ($apiKey, $em): void {
// Ideally this would be a SELECT COUNT(...), but MsSQL and Postgres do not allow locking on aggregates
// Because of that we check if at least one result exists
$firstResult = $em->createQueryBuilder()->select('a.id')
->from(ApiKey::class, 'a')
->setMaxResults(1)
->getQuery()
->setLockMode(LockMode::PESSIMISTIC_WRITE)
->getOneOrNullResult();
if ($firstResult === null) {
$em->persist(ApiKey::fromKey($apiKey));
$em->flush();
}
});
}
}

View file

@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Rest\ApiKey\Repository;
use Doctrine\Persistence\ObjectRepository;
use Happyr\DoctrineSpecification\Repository\EntitySpecificationRepositoryInterface;
interface ApiKeyRepositoryInterface extends ObjectRepository, EntitySpecificationRepositoryInterface
{
/**
* Will create provided API key only if there's no API keys yet
*/
public function createInitialApiKey(string $apiKey): void;
}

View file

@ -23,16 +23,14 @@ class ApiKey extends AbstractEntity
private bool $enabled;
/** @var Collection|ApiKeyRole[] */
private Collection $roles;
private ?string $name;
private ?string $name = null;
/**
* @throws Exception
*/
private function __construct(?string $name = null, ?Chronos $expirationDate = null)
private function __construct(?string $key = null)
{
$this->key = Uuid::uuid4()->toString();
$this->expirationDate = $expirationDate;
$this->name = $name;
$this->key = $key ?? Uuid::uuid4()->toString();
$this->enabled = true;
$this->roles = new ArrayCollection();
}
@ -44,7 +42,10 @@ class ApiKey extends AbstractEntity
public static function fromMeta(ApiKeyMeta $meta): self
{
$apiKey = new self($meta->name, $meta->expirationDate);
$apiKey = self::create();
$apiKey->name = $meta->name;
$apiKey->expirationDate = $meta->expirationDate;
foreach ($meta->roleDefinitions as $roleDefinition) {
$apiKey->registerRole($roleDefinition);
}
@ -52,6 +53,11 @@ class ApiKey extends AbstractEntity
return $apiKey;
}
public static function fromKey(string $key): self
{
return new self($key);
}
public function getExpirationDate(): ?Chronos
{
return $this->expirationDate;

View file

@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace ShlinkioDbTest\Shlink\Rest\ApiKey\Repository;
use Shlinkio\Shlink\Rest\ApiKey\Repository\ApiKeyRepository;
use Shlinkio\Shlink\Rest\Entity\ApiKey;
use Shlinkio\Shlink\TestUtils\DbTest\DatabaseTestCase;
class ApiKeyRepositoryTest extends DatabaseTestCase
{
private ApiKeyRepository $repo;
protected function setUp(): void
{
$this->repo = $this->getEntityManager()->getRepository(ApiKey::class);
}
/** @test */
public function initialApiKeyIsCreatedOnlyOfNoApiKeysExistYet(): void
{
self::assertCount(0, $this->repo->findAll());
$this->repo->createInitialApiKey('initial_value');
self::assertCount(1, $this->repo->findAll());
self::assertCount(1, $this->repo->findBy(['key' => 'initial_value']));
$this->repo->createInitialApiKey('another_one');
self::assertCount(1, $this->repo->findAll());
self::assertCount(0, $this->repo->findBy(['key' => 'another_one']));
}
}

View file

@ -25,7 +25,7 @@ class ListDomainsActionTest extends TestCase
private ObjectProphecy $domainService;
private NotFoundRedirectOptions $options;
public function setUp(): void
protected function setUp(): void
{
$this->domainService = $this->prophesize(DomainServiceInterface::class);
$this->options = new NotFoundRedirectOptions();

View file

@ -25,7 +25,7 @@ class HealthActionTest extends TestCase
private HealthAction $action;
private ObjectProphecy $conn;
public function setUp(): void
protected function setUp(): void
{
$this->conn = $this->prophesize(Connection::class);
$this->conn->executeQuery(Argument::cetera())->willReturn($this->prophesize(Result::class)->reveal());

View file

@ -21,7 +21,7 @@ class MercureInfoActionTest extends TestCase
private ObjectProphecy $provider;
public function setUp(): void
protected function setUp(): void
{
$this->provider = $this->prophesize(JwtProviderInterface::class);
}

View file

@ -29,7 +29,7 @@ class CreateShortUrlActionTest extends TestCase
private ObjectProphecy $urlShortener;
private ObjectProphecy $transformer;
public function setUp(): void
protected function setUp(): void
{
$this->urlShortener = $this->prophesize(UrlShortener::class);
$this->transformer = $this->prophesize(DataTransformerInterface::class);

View file

@ -20,7 +20,7 @@ class DeleteShortUrlActionTest extends TestCase
private DeleteShortUrlAction $action;
private ObjectProphecy $service;
public function setUp(): void
protected function setUp(): void
{
$this->service = $this->prophesize(DeleteShortUrlServiceInterface::class);
$this->action = new DeleteShortUrlAction($this->service->reveal());

View file

@ -24,7 +24,7 @@ class EditShortUrlActionTest extends TestCase
private EditShortUrlAction $action;
private ObjectProphecy $shortUrlService;
public function setUp(): void
protected function setUp(): void
{
$this->shortUrlService = $this->prophesize(ShortUrlServiceInterface::class);
$this->action = new EditShortUrlAction($this->shortUrlService->reveal(), new ShortUrlDataTransformer(

View file

@ -26,7 +26,7 @@ class ListShortUrlsActionTest extends TestCase
private ListShortUrlsAction $action;
private ObjectProphecy $service;
public function setUp(): void
protected function setUp(): void
{
$this->service = $this->prophesize(ShortUrlService::class);

View file

@ -23,7 +23,7 @@ class ResolveShortUrlActionTest extends TestCase
private ResolveShortUrlAction $action;
private ObjectProphecy $urlResolver;
public function setUp(): void
protected function setUp(): void
{
$this->urlResolver = $this->prophesize(ShortUrlResolverInterface::class);
$this->action = new ResolveShortUrlAction($this->urlResolver->reveal(), new ShortUrlDataTransformer(

View file

@ -25,7 +25,7 @@ class SingleStepCreateShortUrlActionTest extends TestCase
private ObjectProphecy $urlShortener;
private ObjectProphecy $transformer;
public function setUp(): void
protected function setUp(): void
{
$this->urlShortener = $this->prophesize(UrlShortenerInterface::class);
$this->transformer = $this->prophesize(DataTransformerInterface::class);

View file

@ -20,7 +20,7 @@ class DeleteTagsActionTest extends TestCase
private DeleteTagsAction $action;
private ObjectProphecy $tagService;
public function setUp(): void
protected function setUp(): void
{
$this->tagService = $this->prophesize(TagServiceInterface::class);
$this->action = new DeleteTagsAction($this->tagService->reveal());

View file

@ -28,7 +28,7 @@ class ListTagsActionTest extends TestCase
private ListTagsAction $action;
private ObjectProphecy $tagService;
public function setUp(): void
protected function setUp(): void
{
$this->tagService = $this->prophesize(TagServiceInterface::class);
$this->action = new ListTagsAction($this->tagService->reveal());

View file

@ -27,7 +27,7 @@ class TagsStatsActionTest extends TestCase
private TagsStatsAction $action;
private ObjectProphecy $tagService;
public function setUp(): void
protected function setUp(): void
{
$this->tagService = $this->prophesize(TagServiceInterface::class);
$this->action = new TagsStatsAction($this->tagService->reveal());

View file

@ -24,7 +24,7 @@ class UpdateTagActionTest extends TestCase
private UpdateTagAction $action;
private ObjectProphecy $tagService;
public function setUp(): void
protected function setUp(): void
{
$this->tagService = $this->prophesize(TagServiceInterface::class);
$this->action = new UpdateTagAction($this->tagService->reveal());

View file

@ -21,7 +21,7 @@ class GlobalVisitsActionTest extends TestCase
private GlobalVisitsAction $action;
private ObjectProphecy $helper;
public function setUp(): void
protected function setUp(): void
{
$this->helper = $this->prophesize(VisitsStatsHelperInterface::class);
$this->action = new GlobalVisitsAction($this->helper->reveal());

View file

@ -24,7 +24,7 @@ class NonOrphanVisitsActionTest extends TestCase
private NonOrphanVisitsAction $action;
private ObjectProphecy $visitsHelper;
public function setUp(): void
protected function setUp(): void
{
$this->visitsHelper = $this->prophesize(VisitsStatsHelperInterface::class);
$this->action = new NonOrphanVisitsAction($this->visitsHelper->reveal());

View file

@ -27,7 +27,7 @@ class ShortUrlVisitsActionTest extends TestCase
private ShortUrlVisitsAction $action;
private ObjectProphecy $visitsHelper;
public function setUp(): void
protected function setUp(): void
{
$this->visitsHelper = $this->prophesize(VisitsStatsHelperInterface::class);
$this->action = new ShortUrlVisitsAction($this->visitsHelper->reveal());

View file

@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\Rest\ApiKey;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\EntityManagerInterface;
use Mezzio\Application;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use Prophecy\Prophecy\ObjectProphecy;
use Psr\Container\ContainerInterface;
use Shlinkio\Shlink\Rest\ApiKey\InitialApiKeyDelegator;
use Shlinkio\Shlink\Rest\ApiKey\Repository\ApiKeyRepositoryInterface;
use Shlinkio\Shlink\Rest\Entity\ApiKey;
class InitialApiKeyDelegatorTest extends TestCase
{
use ProphecyTrait;
private InitialApiKeyDelegator $delegator;
private ObjectProphecy $container;
protected function setUp(): void
{
$this->delegator = new InitialApiKeyDelegator();
$this->container = $this->prophesize(ContainerInterface::class);
}
/**
* @test
* @dataProvider provideConfigs
*/
public function apiKeyIsInitializedWhenAppropriate(array $config, int $expectedCalls): void
{
$app = $this->prophesize(Application::class)->reveal();
$apiKeyRepo = $this->prophesize(ApiKeyRepositoryInterface::class);
$em = $this->prophesize(EntityManagerInterface::class);
$getConfig = $this->container->get('config')->willReturn($config);
$getRepo = $em->getRepository(ApiKey::class)->willReturn($apiKeyRepo->reveal());
$getEm = $this->container->get(EntityManager::class)->willReturn($em->reveal());
$result = ($this->delegator)($this->container->reveal(), '', fn () => $app);
self::assertSame($result, $app);
$getConfig->shouldHaveBeenCalledOnce();
$getRepo->shouldHaveBeenCalledTimes($expectedCalls);
$getEm->shouldHaveBeenCalledTimes($expectedCalls);
$apiKeyRepo->createInitialApiKey(Argument::any())->shouldHaveBeenCalledTimes($expectedCalls);
}
public function provideConfigs(): iterable
{
yield [[], 0];
yield [['initial_api_key' => null], 0];
yield [['initial_api_key' => 'the_initial_key'], 1];
}
}

View file

@ -12,7 +12,7 @@ class ConfigProviderTest extends TestCase
{
private ConfigProvider $configProvider;
public function setUp(): void
protected function setUp(): void
{
$this->configProvider = new ConfigProvider();
}
@ -22,10 +22,11 @@ class ConfigProviderTest extends TestCase
{
$config = ($this->configProvider)();
self::assertCount(4, $config);
self::assertCount(5, $config);
self::assertArrayHasKey('dependencies', $config);
self::assertArrayHasKey('auth', $config);
self::assertArrayHasKey('entity_manager', $config);
self::assertArrayHasKey('initial_api_key', $config);
self::assertArrayHasKey(ConfigAbstractFactory::class, $config);
}

View file

@ -35,7 +35,7 @@ class AuthenticationMiddlewareTest extends TestCase
private ObjectProphecy $apiKeyService;
private ObjectProphecy $handler;
public function setUp(): void
protected function setUp(): void
{
$this->apiKeyService = $this->prophesize(ApiKeyServiceInterface::class);
$this->middleware = new AuthenticationMiddleware(

View file

@ -23,7 +23,7 @@ class BodyParserMiddlewareTest extends TestCase
private BodyParserMiddleware $middleware;
public function setUp(): void
protected function setUp(): void
{
$this->middleware = new BodyParserMiddleware();
}

View file

@ -20,7 +20,7 @@ class CrossDomainMiddlewareTest extends TestCase
private CrossDomainMiddleware $middleware;
private ObjectProphecy $handler;
public function setUp(): void
protected function setUp(): void
{
$this->middleware = new CrossDomainMiddleware(['max_age' => 1000]);
$this->handler = $this->prophesize(RequestHandlerInterface::class);

View file

@ -15,7 +15,7 @@ class EmptyResponseImplicitOptionsMiddlewareFactoryTest extends TestCase
{
private EmptyResponseImplicitOptionsMiddlewareFactory $factory;
public function setUp(): void
protected function setUp(): void
{
$this->factory = new EmptyResponseImplicitOptionsMiddlewareFactory();
}

View file

@ -22,7 +22,7 @@ class CreateShortUrlContentNegotiationMiddlewareTest extends TestCase
private CreateShortUrlContentNegotiationMiddleware $middleware;
private ObjectProphecy $requestHandler;
public function setUp(): void
protected function setUp(): void
{
$this->middleware = new CreateShortUrlContentNegotiationMiddleware();
$this->requestHandler = $this->prophesize(RequestHandlerInterface::class);

View file

@ -23,7 +23,7 @@ class DefaultShortCodesLengthMiddlewareTest extends TestCase
private DefaultShortCodesLengthMiddleware $middleware;
private ObjectProphecy $handler;
public function setUp(): void
protected function setUp(): void
{
$this->handler = $this->prophesize(RequestHandlerInterface::class);
$this->middleware = new DefaultShortCodesLengthMiddleware(8);

View file

@ -22,7 +22,7 @@ class DropDefaultDomainFromRequestMiddlewareTest extends TestCase
private DropDefaultDomainFromRequestMiddleware $middleware;
private ObjectProphecy $next;
public function setUp(): void
protected function setUp(): void
{
$this->next = $this->prophesize(RequestHandlerInterface::class);
$this->middleware = new DropDefaultDomainFromRequestMiddleware('doma.in');

View file

@ -25,7 +25,7 @@ class ApiKeyServiceTest extends TestCase
private ApiKeyService $service;
private ObjectProphecy $em;
public function setUp(): void
protected function setUp(): void
{
$this->em = $this->prophesize(EntityManager::class);
$this->service = new ApiKeyService($this->em->reveal());