From 168fa860f06da4960279ecbf659cb4cbf493d33e Mon Sep 17 00:00:00 2001 From: Marko Ivancic <cicnavi@gmail.com> Date: Wed, 9 Nov 2022 15:02:06 +0100 Subject: [PATCH] Enable Redis jobs store (#10) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Marko Ivančić <marko.ivancic@srce.hr> --- bin/test.php | 140 +++++++ composer.json | 3 +- config-templates/module_accounting.php | 22 +- src/Stores/Bases/AbstractStore.php | 64 +++ .../Bases/DoctrineDbal/AbstractStore.php | 38 +- .../DoctrineDbal/Versioned/Store.php | 5 +- src/Stores/Interfaces/DataStoreInterface.php | 3 +- src/Stores/Interfaces/JobsStoreInterface.php | 16 +- src/Stores/Jobs/DoctrineDbal/Store.php | 14 +- src/Stores/Jobs/PhpRedis/RedisStore.php | 189 +++++++++ tests/src/Constants/StateArrays.php | 40 +- tests/src/Helpers/EnvironmentHelperTest.php | 6 +- tests/src/Helpers/ModuleRoutesHelperTest.php | 3 + .../AuthenticationDataProviderBuilderTest.php | 1 + tests/src/Stores/Bases/AbstractStoreTest.php | 63 +++ .../Stores/Builders/DataStoreBuilderTest.php | 1 + .../Stores/Builders/JobsStoreBuilderTest.php | 1 + .../Versioned/Store/RawActivityTest.php | 1 + .../DoctrineDbal/Versioned/StoreTest.php | 59 +-- .../DoctrineDbal/Store/RepositoryTest.php | 9 +- .../Stores/Jobs/DoctrineDbal/StoreTest.php | 91 ++++- .../Stores/Jobs/PhpRedis/RedisStoreTest.php | 377 ++++++++++++++++++ .../DoctrineDbal/Versioned/TrackerTest.php | 1 + 23 files changed, 1013 insertions(+), 134 deletions(-) create mode 100644 bin/test.php create mode 100644 src/Stores/Bases/AbstractStore.php create mode 100644 src/Stores/Jobs/PhpRedis/RedisStore.php create mode 100644 tests/src/Stores/Bases/AbstractStoreTest.php create mode 100644 tests/src/Stores/Jobs/PhpRedis/RedisStoreTest.php diff --git a/bin/test.php b/bin/test.php new file mode 100644 index 0000000..8880791 --- /dev/null +++ b/bin/test.php @@ -0,0 +1,140 @@ +#!/usr/bin/env php +<?php +// TODO mivanci remove this file +declare(strict_types=1); + +use SimpleSAML\Module\accounting\Entities\Authentication\Event; +use SimpleSAML\Module\accounting\Entities\Authentication\Event\Job; +use SimpleSAML\Module\accounting\Entities\Authentication\State; +use SimpleSAML\Module\accounting\Services\HelpersManager; +use SimpleSAML\Module\accounting\Services\Logger; +use SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Connection; +use SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\Store\Repository; +use SimpleSAML\Module\accounting\Stores\Jobs\PhpRedis\RedisStore; + +require 'vendor/autoload.php'; + +$helpersManager = new HelpersManager(); + +$start = new DateTime(); + +$newLine = "\n"; + +echo "Start: " . $start->format(DateTime::ATOM); +echo $newLine; + +$job = new Job(new Event(new State(SimpleSAML\Test\Module\accounting\Constants\StateArrays::FULL))); + +$options = getopt('c:'); + +$numberOfItems = $options['c'] ?? 1000; + +echo 'Number of items: ' . $numberOfItems; +echo $newLine; + +$spinnerChars = ['|', '/', '-', '\\']; + +/** +echo 'Starting simulating MySQL: '; +$mysqlStartTime = new DateTime(); +echo $mysqlStartTime->format(DateTime::ATOM); +echo $newLine; + +$mysqlParameters = [ + 'driver' => 'pdo_mysql', // (string): The built-in driver implementation to use + 'user' => 'apps', // (string): Username to use when connecting to the database. + 'password' => 'apps', // (string): Password to use when connecting to the database. + 'host' => '127.0.0.1', // (string): Hostname of the database to connect to. + 'port' => 33306, // (integer): Port of the database to connect to. + 'dbname' => 'accounting', // (string): Name of the database/schema to connect to. + //'unix_socket' => 'unix_socet', // (string): Name of the socket used to connect to the database. + 'charset' => 'utf8', // (string): The charset used when connecting to the database. + //'url' => 'mysql://user:secret@localhost/mydb?charset=utf8', // ...alternative way of providing parameters. + // Additional parameters not originally avaliable in Doctrine DBAL + 'table_prefix' => '', // (string): Prefix for each table. +]; + +$logger = new Logger(); + +$jobsStoreRepository = new Repository(new Connection($mysqlParameters), 'job', $logger); +$mysqlDurationInSeconds = (new DateTime())->getTimestamp() - $mysqlStartTime->getTimestamp(); +$mysqlItemsInCurrentSecond = 0; +$mysqlItemsPerSecond = []; +for ($i = 1; $i <= $numberOfItems; $i++) { + $mysqlUpdatedDurationInSeconds = (new DateTime())->getTimestamp() - $mysqlStartTime->getTimestamp(); + if ($mysqlDurationInSeconds === $mysqlUpdatedDurationInSeconds) { + $mysqlItemsInCurrentSecond++; + } else { + $mysqlItemsPerSecond[] = $mysqlItemsInCurrentSecond; + $mysqlItemsInCurrentSecond = 0; + } + $mysqlItemsInCurrentSecond = $mysqlDurationInSeconds === $mysqlUpdatedDurationInSeconds ? + $mysqlItemsInCurrentSecond++ : 0; + $mysqlDurationInSeconds = (new DateTime())->getTimestamp() - $mysqlStartTime->getTimestamp(); + + $mysqlItemsPerSeconds = count($mysqlItemsPerSecond) ? + array_sum($mysqlItemsPerSecond) / count($mysqlItemsPerSecond) : 0; + $mysqlPercentage = $i / $numberOfItems * 100; + $spinnerChar = $spinnerChars[array_rand($spinnerChars)]; + $line = sprintf( + '%1$s percentage: %2$ 3d%%, items/s: %3$04d, duration: %4$ss', + $spinnerChar, $mysqlPercentage, $mysqlItemsPerSeconds, $mysqlDurationInSeconds + ); + echo $line; + echo "\r"; + $jobsStoreRepository->insert($job); +} +echo $newLine; +echo $newLine; + +*/ +echo 'Starting simulating Redis: '; +$redisStartTime = new DateTime(); +echo $redisStartTime->format(DateTime::ATOM); +echo $newLine; + +$redisClient = new Redis(); +$redisClient->connect( + '127.0.0.1', + 6379, + 1, + null, + 500, + 1 +); +$redisClient->auth('apps'); +$redisClient->setOption(Redis::OPT_PREFIX, 'ssp_accounting:'); + + +$redisDurationInSeconds = (new DateTime())->getTimestamp() - $redisStartTime->getTimestamp(); +$redisItemsInCurrentSecond = 0; +$redisItemsPerSecond = []; +for ($i = 1; $i <= $numberOfItems; $i++) { + $redisUpdatedDurationInSeconds = (new DateTime())->getTimestamp() - $redisStartTime->getTimestamp(); + if ($redisDurationInSeconds === $redisUpdatedDurationInSeconds) { + $redisItemsInCurrentSecond++; + } else { + $redisItemsPerSecond[] = $redisItemsInCurrentSecond; + $redisItemsInCurrentSecond = 0; + } + $redisItemsInCurrentSecond = $redisDurationInSeconds === $redisUpdatedDurationInSeconds ? + $redisItemsInCurrentSecond++ : 0; + + $redisDurationInSeconds = $redisUpdatedDurationInSeconds; + + $redisItemsPerSeconds = count($redisItemsPerSecond) ? + array_sum($redisItemsPerSecond) / count($redisItemsPerSecond) : 0; + $redisPercentage = $i / $numberOfItems * 100; + $spinnerChar = $spinnerChars[array_rand($spinnerChars)]; + $line = sprintf( + '%1$s percentage: %2$ 3d%%, items/s: %3$04d, duration: %4$ss', + $spinnerChar, $redisPercentage, $redisItemsPerSeconds, $redisDurationInSeconds + ); + echo $line; + echo "\r"; + $redisClient->rPush(RedisStore::LIST_KEY_JOB . ':' . sha1($job->getType()), serialize($job)); +// $redisClient->rPush(RedisStore::LIST_KEY_JOB, serializgit add .e($job)); +} +echo $newLine; +echo 'End: ' . (new DateTime())->format(DateTime::ATOM); +echo $newLine; \ No newline at end of file diff --git a/composer.json b/composer.json index 81c480e..c7e7ffa 100644 --- a/composer.json +++ b/composer.json @@ -41,7 +41,8 @@ "simplesamlphp/simplesamlphp-test-framework": "^1" }, "suggest": { - "ext-pcntl": "*" + "ext-pcntl": "Enables job runner to gracefully respond to SIGTERM signal.", + "ext-redis": "Mandatory if PhpRedis is to be used as a store." }, "scripts": { "pre-commit": [ diff --git a/config-templates/module_accounting.php b/config-templates/module_accounting.php index 17f8a52..62e9a4e 100644 --- a/config-templates/module_accounting.php +++ b/config-templates/module_accounting.php @@ -57,6 +57,11 @@ $config = [ * Default jobs store class which expects Doctrine DBAL compatible connection to be set below. */ Stores\Jobs\DoctrineDbal\Store::class, + /** + * PhpRedis class Redis jobs store. Expects class Redis compatible connection to be set bellow. + * Note: PhpRedis must be installed: https://github.com/phpredis/phpredis#installation + */ + //Stores\Jobs\PhpRedis\RedisStore::class, /** * Default data tracker and provider to be used for accounting and as a source for data display in SSP UI. @@ -98,6 +103,7 @@ $config = [ * Connection key to be used by jobs store class. */ Stores\Jobs\DoctrineDbal\Store::class => 'doctrine_dbal_pdo_mysql', + Stores\Jobs\PhpRedis\RedisStore::class => 'phpredis_class_redis', /** * Connection key to be used by this data tracker and provider. */ @@ -119,7 +125,7 @@ $config = [ * There are additional parameters for: table prefix. */ 'doctrine_dbal_pdo_mysql' => [ - 'driver' => 'pdo_mysql', // (string): The built-in driver implementation to use + 'driver' => 'pdo_mysql', // (string): The built-in driver implementation to use. 'user' => 'user', // (string): Username to use when connecting to the database. 'password' => 'password', // (string): Password to use when connecting to the database. 'host' => 'host', // (string): Hostname of the database to connect to. @@ -132,7 +138,7 @@ $config = [ 'table_prefix' => '', // (string): Prefix for each table. ], 'doctrine_dbal_pdo_sqlite' => [ - 'driver' => 'pdo_sqlite', // (string): The built-in driver implementation to use + 'driver' => 'pdo_sqlite', // (string): The built-in driver implementation to use. 'path' => '/path/to/db.sqlite', // (string): The filesystem path to the database file. // Mutually exclusive with memory. path takes precedence. 'memory' => false, // (boolean): True if the SQLite database should be in-memory (non-persistent). @@ -142,6 +148,18 @@ $config = [ // Additional parameters not originally available in Doctrine DBAL 'table_prefix' => '', // (string): Prefix for each table. ], + /** + * Example for PhpRedis class Redis (https://github.com/phpredis/phpredis#class-redis). + */ + 'phpredis_class_redis' => [ + 'host' => '127.0.0.1', // (string): can be a host, or the path to a unix domain socket. + 'port' => 6379, // (int): default port is 6379, should be -1 for unix domain socket. + 'connectTimeout' => 1, // (float): value in seconds (default is 0 meaning unlimited). + //'retryInterval' => 500, // (int): value in milliseconds (optional, default 0) + //'readTimeout' => 0, // (float): value in seconds (default is 0 meaning unlimited) + 'auth' => ['phpredis', 'phpredis'], // (mixed): authentication information + 'keyPrefix' => 'ssp_accounting:' + ], ], /** diff --git a/src/Stores/Bases/AbstractStore.php b/src/Stores/Bases/AbstractStore.php new file mode 100644 index 0000000..09b5ff6 --- /dev/null +++ b/src/Stores/Bases/AbstractStore.php @@ -0,0 +1,64 @@ +<?php + +declare(strict_types=1); + +namespace SimpleSAML\Module\accounting\Stores\Bases; + +use Psr\Log\LoggerInterface; +use ReflectionClass; +use SimpleSAML\Module\accounting\Interfaces\BuildableUsingModuleConfigurationInterface; +use SimpleSAML\Module\accounting\Interfaces\SetupableInterface; +use SimpleSAML\Module\accounting\ModuleConfiguration; + +abstract class AbstractStore implements BuildableUsingModuleConfigurationInterface, SetupableInterface +{ + protected ModuleConfiguration $moduleConfiguration; + protected LoggerInterface $logger; + protected string $connectionKey; + + /** + */ + public function __construct( + ModuleConfiguration $moduleConfiguration, + LoggerInterface $logger, + string $connectionKey = null, + string $connectionType = ModuleConfiguration\ConnectionType::MASTER + ) { + $this->moduleConfiguration = $moduleConfiguration; + $this->logger = $logger; + + $this->connectionKey = $connectionKey ?? + $moduleConfiguration->getClassConnectionKey($this->getSelfClass(), $connectionType); + } + + /** + * Get ReflectionClass of current store instance. + * @return ReflectionClass + */ + protected function getReflection(): ReflectionClass + { + return new ReflectionClass($this); + } + + /** + * Get class of the current store instance. + * @return string + */ + protected function getSelfClass(): string + { + return $this->getReflection()->getName(); + } + + /** + * Build store instance. Must be implemented in child classes for proper return store type. + * @param ModuleConfiguration $moduleConfiguration + * @param LoggerInterface $logger + * @param string|null $connectionKey + * @return self + */ + abstract public static function build( + ModuleConfiguration $moduleConfiguration, + LoggerInterface $logger, + string $connectionKey = null + ): self; +} diff --git a/src/Stores/Bases/DoctrineDbal/AbstractStore.php b/src/Stores/Bases/DoctrineDbal/AbstractStore.php index f560fac..7dabc98 100644 --- a/src/Stores/Bases/DoctrineDbal/AbstractStore.php +++ b/src/Stores/Bases/DoctrineDbal/AbstractStore.php @@ -8,20 +8,17 @@ use Psr\Log\LoggerInterface; use ReflectionClass; use SimpleSAML\Module\accounting\Exceptions\StoreException; use SimpleSAML\Module\accounting\Exceptions\StoreException\MigrationException; -use SimpleSAML\Module\accounting\Interfaces\BuildableUsingModuleConfigurationInterface; -use SimpleSAML\Module\accounting\Interfaces\SetupableInterface; use SimpleSAML\Module\accounting\ModuleConfiguration; use SimpleSAML\Module\accounting\Stores\Connections\Bases\AbstractMigrator; use SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Connection; use SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Factory; use SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Migrator; -abstract class AbstractStore implements BuildableUsingModuleConfigurationInterface, SetupableInterface +abstract class AbstractStore extends \SimpleSAML\Module\accounting\Stores\Bases\AbstractStore { - protected ModuleConfiguration $moduleConfiguration; protected Connection $connection; protected Migrator $migrator; - protected LoggerInterface $logger; + protected Factory $connectionFactory; /** * @throws StoreException @@ -29,35 +26,16 @@ abstract class AbstractStore implements BuildableUsingModuleConfigurationInterfa public function __construct( ModuleConfiguration $moduleConfiguration, LoggerInterface $logger, - Factory $connectionFactory, string $connectionKey = null, - string $connectionType = ModuleConfiguration\ConnectionType::MASTER + string $connectionType = ModuleConfiguration\ConnectionType::MASTER, + Factory $connectionFactory = null ) { - $this->moduleConfiguration = $moduleConfiguration; - $this->logger = $logger; + parent::__construct($moduleConfiguration, $logger, $connectionKey, $connectionType); - $connectionKey = $connectionKey ?? - $moduleConfiguration->getClassConnectionKey($this->getSelfClass(), $connectionType); - $this->connection = $connectionFactory->buildConnection($connectionKey); - $this->migrator = $connectionFactory->buildMigrator($this->connection); - } - - /** - * Get ReflectionClass of current store instance. - * @return ReflectionClass - */ - protected function getReflection(): ReflectionClass - { - return new ReflectionClass($this); - } + $this->connectionFactory = $connectionFactory ?? new Factory($this->moduleConfiguration, $this->logger); - /** - * Get class of the current store instance. - * @return string - */ - protected function getSelfClass(): string - { - return $this->getReflection()->getName(); + $this->connection = $this->connectionFactory->buildConnection($this->connectionKey); + $this->migrator = $this->connectionFactory->buildMigrator($this->connection); } protected function getMigrationsNamespace(): string diff --git a/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store.php b/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store.php index 8bfaed5..37319f1 100644 --- a/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store.php +++ b/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store.php @@ -33,13 +33,13 @@ class Store extends AbstractStore implements DataStoreInterface public function __construct( ModuleConfiguration $moduleConfiguration, LoggerInterface $logger, - Factory $connectionFactory, string $connectionKey = null, string $connectionType = ModuleConfiguration\ConnectionType::MASTER, + Factory $connectionFactory = null, Repository $repository = null, HelpersManager $helpersManager = null ) { - parent::__construct($moduleConfiguration, $logger, $connectionFactory, $connectionKey, $connectionType); + parent::__construct($moduleConfiguration, $logger, $connectionKey, $connectionType, $connectionFactory); $this->repository = $repository ?? new Repository($this->connection, $this->logger); $this->helpersManager = $helpersManager ?? new HelpersManager(); @@ -58,7 +58,6 @@ class Store extends AbstractStore implements DataStoreInterface return new self( $moduleConfiguration, $logger, - new Factory($moduleConfiguration, $logger), $connectionKey, $connectionType ); diff --git a/src/Stores/Interfaces/DataStoreInterface.php b/src/Stores/Interfaces/DataStoreInterface.php index 8b5e0e5..7ba0816 100644 --- a/src/Stores/Interfaces/DataStoreInterface.php +++ b/src/Stores/Interfaces/DataStoreInterface.php @@ -15,7 +15,8 @@ interface DataStoreInterface extends StoreInterface public static function build( ModuleConfiguration $moduleConfiguration, LoggerInterface $logger, - string $connectionKey = null + string $connectionKey = null, + string $connectionType = ModuleConfiguration\ConnectionType::MASTER ): self; public function persist(Event $authenticationEvent): void; diff --git a/src/Stores/Interfaces/JobsStoreInterface.php b/src/Stores/Interfaces/JobsStoreInterface.php index 90d05c4..43f9bdb 100644 --- a/src/Stores/Interfaces/JobsStoreInterface.php +++ b/src/Stores/Interfaces/JobsStoreInterface.php @@ -10,6 +10,12 @@ use SimpleSAML\Module\accounting\ModuleConfiguration; interface JobsStoreInterface extends StoreInterface { + public static function build( + ModuleConfiguration $moduleConfiguration, + LoggerInterface $logger, + string $connectionKey = null + ): self; + /** * Add job to queue * @param JobInterface $job @@ -19,20 +25,14 @@ interface JobsStoreInterface extends StoreInterface /** * Get job from queue - * @param string|null $type + * @param string $type Type of the job, typically FQ class name of job object. * @return ?JobInterface */ - public function dequeue(string $type = null): ?JobInterface; + public function dequeue(string $type): ?JobInterface; /** * @param JobInterface $job * @return void */ public function markFailedJob(JobInterface $job): void; - - public static function build( - ModuleConfiguration $moduleConfiguration, - LoggerInterface $logger, - string $connectionKey = null - ): self; } diff --git a/src/Stores/Jobs/DoctrineDbal/Store.php b/src/Stores/Jobs/DoctrineDbal/Store.php index 55ba996..86107ec 100644 --- a/src/Stores/Jobs/DoctrineDbal/Store.php +++ b/src/Stores/Jobs/DoctrineDbal/Store.php @@ -29,13 +29,13 @@ class Store extends AbstractStore implements JobsStoreInterface public function __construct( ModuleConfiguration $moduleConfiguration, LoggerInterface $logger, - Factory $connectionFactory, string $connectionKey = null, string $connectionType = ModuleConfiguration\ConnectionType::MASTER, + Factory $connectionFactory = null, Repository $jobsRepository = null, Repository $failedJobsRepository = null ) { - parent::__construct($moduleConfiguration, $logger, $connectionFactory, $connectionKey, $connectionType); + parent::__construct($moduleConfiguration, $logger, $connectionKey, $connectionType, $connectionFactory); $this->prefixedTableNameJobs = $this->connection->preparePrefixedTableName(TableConstants::TABLE_NAME_JOB); $this->prefixedTableNameFailedJobs = $this->connection @@ -59,7 +59,7 @@ class Store extends AbstractStore implements JobsStoreInterface /** * @throws StoreException */ - public function dequeue(string $type = null): ?JobInterface + public function dequeue(string $type): ?JobInterface { /** @noinspection PhpUnusedLocalVariableInspection - psalm reports possibly undefined variable */ $job = null; @@ -127,21 +127,19 @@ class Store extends AbstractStore implements JobsStoreInterface * @param ModuleConfiguration $moduleConfiguration * @param LoggerInterface $logger * @param string|null $connectionKey + * @param string $connectionType * @return self * @throws StoreException */ public static function build( ModuleConfiguration $moduleConfiguration, LoggerInterface $logger, - string $connectionKey = null, - string $connectionType = ModuleConfiguration\ConnectionType::MASTER + string $connectionKey = null ): self { return new self( $moduleConfiguration, $logger, - new Factory($moduleConfiguration, $logger), - $connectionKey, - $connectionType + $connectionKey ); } diff --git a/src/Stores/Jobs/PhpRedis/RedisStore.php b/src/Stores/Jobs/PhpRedis/RedisStore.php new file mode 100644 index 0000000..a2cfbfc --- /dev/null +++ b/src/Stores/Jobs/PhpRedis/RedisStore.php @@ -0,0 +1,189 @@ +<?php + +declare(strict_types=1); + +namespace SimpleSAML\Module\accounting\Stores\Jobs\PhpRedis; + +use Psr\Log\LoggerInterface; +use Redis; +use SimpleSAML\Module\accounting\Entities\Interfaces\JobInterface; +use SimpleSAML\Module\accounting\Exceptions\InvalidConfigurationException; +use SimpleSAML\Module\accounting\Exceptions\StoreException; +use SimpleSAML\Module\accounting\ModuleConfiguration; +use SimpleSAML\Module\accounting\Stores\Bases\AbstractStore; +use SimpleSAML\Module\accounting\Stores\Interfaces\JobsStoreInterface; +use Throwable; + +class RedisStore extends AbstractStore implements JobsStoreInterface +{ + public const LIST_KEY_JOB = 'job'; + public const LIST_KEY_JOB_FAILED = 'job_failed'; + + protected Redis $redis; + + /** + * @throws StoreException + */ + public function __construct( + ModuleConfiguration $moduleConfiguration, + LoggerInterface $logger, + string $connectionKey = null, + string $connectionType = ModuleConfiguration\ConnectionType::MASTER, + Redis $redis = null + ) { + parent::__construct($moduleConfiguration, $logger, $connectionKey, $connectionType); + $this->redis = $redis ?? new Redis(); + $connectionParameters = $this->getConnectionParameters(); + + try { + if (!$this->redis->isConnected()) { + $this->redis->connect( + (string)($connectionParameters['host'] ?? ''), + (int)($connectionParameters['port'] ?? 6379), + (float)($connectionParameters['connectTimeout'] ?? 0.0), + null, + (int)($connectionParameters['retryInterval'] ?? 0), + (int)($connectionParameters['readTimeout'] ?? 0), + ); + } + } catch (Throwable $exception) { + $message = sprintf('Error trying to connect to Redis DB. Error was: %s', $exception->getMessage()); + $this->logger->error($message); + throw new StoreException($message, (int) $exception->getCode(), $exception); + } + + try { + if (isset($connectionParameters['auth'])) { + $this->redis->auth($connectionParameters['auth']); + } + } catch (Throwable $exception) { + $message = sprintf('Error trying to set auth parameter for Redis. Error was: %s', $exception->getMessage()); + $this->logger->error($message); + throw new StoreException($message); + } + + try { + $this->redis->setOption(Redis::OPT_PREFIX, $connectionParameters['keyPrefix'] ?? 'ssp_accounting:'); + } catch (Throwable $exception) { + $message = sprintf('Could not set key prefix for Redis. Error was: %s', $exception->getMessage()); + $this->logger->error($message); + throw new StoreException($message, (int)$exception->getCode(), $exception); + } + } + + /** + * @inheritDoc + * @throws StoreException + */ + public function enqueue(JobInterface $job): void + { + try { + $listKey = $this->resolveListKeyForType(self::LIST_KEY_JOB, $job->getType()); + $this->redis->rPush($listKey, serialize($job)); + } catch (Throwable $exception) { + $message = sprintf('Could not add job to Redis list. Error was: %s', $exception->getMessage()); + $this->logger->error($message); + throw new StoreException($message); + } + } + + /** + * @inheritDoc + * @throws StoreException + */ + public function dequeue(string $type): ?JobInterface + { + try { + $listKey = $this->resolveListKeyForType(self::LIST_KEY_JOB, $type); + if (!is_string($serializedJob = $this->redis->lPop($listKey))) { + return null; + } + } catch (Throwable $exception) { + $message = sprintf('Could not pop job from Redis list. Error was: %s', $exception->getMessage()); + $this->logger->error($message); + throw new StoreException($message); + } + + /** @var JobInterface|false $job */ + $job = unserialize($serializedJob); + + if ($job instanceof JobInterface) { + return $job; + } + + $message = sprintf( + 'Could not deserialize job entry which was available in Redis. Entry was %s.', + $serializedJob + ); + $this->logger->error($message); + throw new StoreException($message); + } + + /** + * @inheritDoc + * @throws StoreException + */ + public function markFailedJob(JobInterface $job): void + { + try { + $listKey = $this->resolveListKeyForType(self::LIST_KEY_JOB_FAILED, $job->getType()); + $this->redis->rPush($listKey, serialize($job)); + } catch (Throwable $exception) { + $message = sprintf('Could not mark job as failed. Error was: %s', $exception->getMessage()); + $this->logger->error($message); + throw new StoreException($message); + } + } + + /** + * @throws StoreException + */ + public static function build( + ModuleConfiguration $moduleConfiguration, + LoggerInterface $logger, + string $connectionKey = null + ): self { + return new self( + $moduleConfiguration, + $logger, + $connectionKey + ); + } + + public function needsSetup(): bool + { + return false; + } + + public function runSetup(): void + { + // No need for setup. + } + + /** + * @return array + * @throws InvalidConfigurationException + */ + protected function getConnectionParameters(): array + { + $connectionParameters = $this->moduleConfiguration->getConnectionParameters($this->connectionKey); + + if (!isset($connectionParameters['host'])) { + $message = 'PhpRedis class Redis expects at least host option to be set, none given.'; + $this->logger->error($message); + throw new InvalidConfigurationException($message); + } + + return $connectionParameters; + } + + /** + * @param string $list For example, job, job_failed... + * @param string $jobType For example, FQ class name of the job instance + * @return string Key with hashed type to conserve chars. + */ + protected function resolveListKeyForType(string $list, string $jobType): string + { + return $list . ':' . sha1($jobType); + } +} diff --git a/tests/src/Constants/StateArrays.php b/tests/src/Constants/StateArrays.php index 7ad8ba4..e18b082 100644 --- a/tests/src/Constants/StateArrays.php +++ b/tests/src/Constants/StateArrays.php @@ -43,6 +43,8 @@ final class StateArrays 'entityid' => 'https://pc-example.org.hr:9074/simplesamlphp/simplesamlphp-2-beta-git/module.php/saml/sp/metadata.php/default-sp', 'metadata-index' => 'https://pc-example.org.hr:9074/simplesamlphp/simplesamlphp-2-beta-git/module.php/saml/sp/metadata.php/default-sp', 'metadata-set' => 'saml20-sp-remote', + 'name' => 'Test service', + 'description' => 'Test service description' ], 'saml:RelayState' => 'https://localhost.someone.from.hr:9074/simplesamlphp/simplesamlphp-2-beta-git/module.php/admin/test/default-sp', 'saml:RequestId' => null, @@ -75,50 +77,12 @@ final class StateArrays 'Attributes' => [ 'hrEduPersonUniqueID' => [0 => 'testuser@primjer.hr',], 'urn:oid:0.9.2342.19200300.100.1.1' => [0 => 'testuser',], - 'urn:oid:2.5.4.3' => [0 => 'TestName TestSurname',], 'urn:oid:2.5.4.4' => [0 => 'TestSurname', 1 => 'TestSurname2',], 'urn:oid:2.5.4.42' => [0 => 'TestName',], - 'urn:oid:0.9.2342.19200300.100.1.3' => [0 => 'testusermail@primjer.hr', 1 => 'testusermail2@primjer.hr',], - 'urn:oid:2.5.4.20' => [0 => '123',], - 'hrEduPersonExtensionNumber' => [0 => '123',], - 'urn:oid:0.9.2342.19200300.100.1.41' => [0 => '123',], - 'urn:oid:2.5.4.23' => [0 => '123',], - 'hrEduPersonUniqueNumber' => [0 => 'LOCAL_NO: 100 ', 1 => 'OIB: 00861590360',], - 'hrEduPersonOIB' => [0 => '00861590360',], - 'hrEduPersonDateOfBirth' => [0 => '20200101',], - 'hrEduPersonGender' => [0 => '9',], - 'urn:oid:0.9.2342.19200300.100.1.60' => [0 => '987654321',], - 'urn:oid:2.5.4.36' => [0 => 'cert-value',], - 'urn:oid:1.3.6.1.4.1.250.1.57' => [0 => 'http://www.org.hr/~ivek Home Page',], - 'hrEduPersonProfessionalStatus' => [0 => 'VSS',], - 'hrEduPersonAcademicStatus' => [0 => 'redoviti profesor',], - 'hrEduPersonScienceArea' => [0 => 'sociologija',], - 'hrEduPersonAffiliation' => [0 => 'djelatnik', 1 => 'student',], - 'hrEduPersonPrimaryAffiliation' => [0 => 'djelatnik',], - 'hrEduPersonStudentCategory' => [0 => 'redoviti student:diplomski sveučilišni studij',], - 'hrEduPersonExpireDate' => [0 => '20211231',], - 'hrEduPersonTitle' => [0 => 'rektor',], - 'hrEduPersonRole' => [0 => 'ICT koordinator', 1 => 'ISVU koordinator',], - 'hrEduPersonStaffCategory' => [0 => 'nastavno osoblje', 1 => 'istraživači',], - 'hrEduPersonGroupMember' => [0 => 'grupa 1', 1 => 'grupa 2',], 'urn:oid:2.5.4.10' => [0 => 'Testna ustanova',], - 'hrEduPersonHomeOrg' => [0 => 'primjer.hr',], 'urn:oid:2.5.4.11' => [0 => 'Testna org jedinica',], - 'urn:oid:0.9.2342.19200300.100.1.6' => [0 => '123',], - 'urn:oid:2.5.4.16' => [0 => 'Testna ustanova, Baštijanova 52A, HR-10000 Zagreb',], - 'urn:oid:2.5.4.7' => [0 => 'Zagreb',], - 'urn:oid:2.5.4.17' => [0 => 'HR-10000',], - 'urn:oid:2.5.4.9' => [0 => 'Baštijanova 52A',], - 'urn:oid:0.9.2342.19200300.100.1.39' => [0 => 'Testna adresa 52A, HR-10000, Zagreb',], - 'urn:oid:0.9.2342.19200300.100.1.20' => [0 => '123',], - 'hrEduPersonCommURI' => [0 => '123',], - 'hrEduPersonPrivacy' => [0 => 'street', 1 => 'postalCode',], 'hrEduPersonPersistentID' => [0 => 'da4294fb4e5746d57ab6ad88d2daf275',], - 'urn:oid:2.16.840.1.113730.3.1.241' => [0 => 'testname123',], - 'urn:oid:1.3.6.1.4.1.25178.1.2.12' => [0 => 'skype: pepe.perez',], - 'hrEduPersonCardNum' => [0 => '123',], 'updatedAt' => [0 => '123456789',], - 'urn:oid:1.3.6.1.4.1.5923.1.1.1.7' => [0 => 'urn:example:oidc:manage:client',], ], 'Authority' => 'example-userpass', 'AuthnInstant' => 1660911943, diff --git a/tests/src/Helpers/EnvironmentHelperTest.php b/tests/src/Helpers/EnvironmentHelperTest.php index 223ccd1..c6e95ef 100644 --- a/tests/src/Helpers/EnvironmentHelperTest.php +++ b/tests/src/Helpers/EnvironmentHelperTest.php @@ -12,8 +12,8 @@ use PHPUnit\Framework\TestCase; */ class EnvironmentHelperTest extends TestCase { - public function testConfirmIsCli(): void - { + public function testConfirmIsCli(): void + { $this->assertTrue((new EnvironmentHelper())->isCli()); - } + } } diff --git a/tests/src/Helpers/ModuleRoutesHelperTest.php b/tests/src/Helpers/ModuleRoutesHelperTest.php index e9f2059..aa07c47 100644 --- a/tests/src/Helpers/ModuleRoutesHelperTest.php +++ b/tests/src/Helpers/ModuleRoutesHelperTest.php @@ -34,6 +34,7 @@ class ModuleRoutesHelperTest extends TestCase $path = 'sample-path'; $moduleUrlWithPath = $this->moduleUrl . '/' . $path; + /** @psalm-suppress PossiblyInvalidArgument */ $moduleRoutesHelper = new ModuleRoutesHelper($this->sspHttpUtilsStub); $this->assertSame($moduleUrlWithPath, $moduleRoutesHelper->getUrl($path)); @@ -45,7 +46,9 @@ class ModuleRoutesHelperTest extends TestCase $params = ['sample' => 'param']; $fullUrl = 'full-url-with-sample-param'; + /** @psalm-suppress PossiblyUndefinedMethod, MixedMethodCall */ $this->sspHttpUtilsStub->method('addURLParameters')->willReturn($fullUrl); + /** @psalm-suppress PossiblyInvalidArgument */ $moduleRoutesHelper = new ModuleRoutesHelper($this->sspHttpUtilsStub); $this->assertSame($fullUrl, $moduleRoutesHelper->getUrl($path, $params)); diff --git a/tests/src/Providers/Builders/AuthenticationDataProviderBuilderTest.php b/tests/src/Providers/Builders/AuthenticationDataProviderBuilderTest.php index e5c615c..497fd3a 100644 --- a/tests/src/Providers/Builders/AuthenticationDataProviderBuilderTest.php +++ b/tests/src/Providers/Builders/AuthenticationDataProviderBuilderTest.php @@ -27,6 +27,7 @@ use SimpleSAML\Test\Module\accounting\Constants\ConnectionParameters; * @uses \SimpleSAML\Module\accounting\Trackers\Authentication\DoctrineDbal\Versioned\Tracker * @uses \SimpleSAML\Module\accounting\Stores\Connections\Bases\AbstractMigrator * @uses \SimpleSAML\Module\accounting\Services\HelpersManager + * @uses \SimpleSAML\Module\accounting\Stores\Bases\AbstractStore */ class AuthenticationDataProviderBuilderTest extends TestCase { diff --git a/tests/src/Stores/Bases/AbstractStoreTest.php b/tests/src/Stores/Bases/AbstractStoreTest.php new file mode 100644 index 0000000..5580ba7 --- /dev/null +++ b/tests/src/Stores/Bases/AbstractStoreTest.php @@ -0,0 +1,63 @@ +<?php + +namespace SimpleSAML\Test\Module\accounting\Stores\Bases; + +use Psr\Log\LoggerInterface; +use SimpleSAML\Module\accounting\ModuleConfiguration; +use SimpleSAML\Module\accounting\Stores\Bases\AbstractStore; +use PHPUnit\Framework\TestCase; + +/** + * @covers \SimpleSAML\Module\accounting\Stores\Bases\AbstractStore + */ +class AbstractStoreTest extends TestCase +{ + /** + * @var AbstractStore + */ + protected $abstractStore; + /** + * @var \PHPUnit\Framework\MockObject\Stub|ModuleConfiguration + */ + protected $moduleConfigurationStub; + /** + * @var \PHPUnit\Framework\MockObject\Stub|LoggerInterface + */ + protected $loggerStub; + + protected function setUp(): void + { + $this->moduleConfigurationStub = $this->createStub(ModuleConfiguration::class); + $this->loggerStub = $this->createStub(LoggerInterface::class); + + $this->abstractStore = new class ( + $this->moduleConfigurationStub, + $this->loggerStub + ) extends AbstractStore { + public static function build( + ModuleConfiguration $moduleConfiguration, + LoggerInterface $logger, + string $connectionKey = null + ): AbstractStore { + return new self($moduleConfiguration, $logger, $connectionKey); + } + public function needsSetup(): bool + { + return false; + } + public function runSetup(): void + { + } + }; + } + + public function testCanBuildInstance(): void + { + $this->assertInstanceOf(AbstractStore::class, $this->abstractStore); + /** @psalm-suppress PossiblyInvalidArgument */ + $this->assertInstanceOf( + AbstractStore::class, + $this->abstractStore::build($this->moduleConfigurationStub, $this->loggerStub) + ); + } +} diff --git a/tests/src/Stores/Builders/DataStoreBuilderTest.php b/tests/src/Stores/Builders/DataStoreBuilderTest.php index 3bcc50a..c007b88 100644 --- a/tests/src/Stores/Builders/DataStoreBuilderTest.php +++ b/tests/src/Stores/Builders/DataStoreBuilderTest.php @@ -23,6 +23,7 @@ use SimpleSAML\Test\Module\accounting\Constants\ConnectionParameters; * @uses \SimpleSAML\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned\Store\Repository * @uses \SimpleSAML\Module\accounting\Stores\Connections\Bases\AbstractMigrator * @uses \SimpleSAML\Module\accounting\Services\HelpersManager + * @uses \SimpleSAML\Module\accounting\Stores\Bases\AbstractStore */ class DataStoreBuilderTest extends TestCase { diff --git a/tests/src/Stores/Builders/JobsStoreBuilderTest.php b/tests/src/Stores/Builders/JobsStoreBuilderTest.php index 906f345..02166c8 100644 --- a/tests/src/Stores/Builders/JobsStoreBuilderTest.php +++ b/tests/src/Stores/Builders/JobsStoreBuilderTest.php @@ -27,6 +27,7 @@ use SimpleSAML\Test\Module\accounting\Constants\ConnectionParameters; * @uses \SimpleSAML\Module\accounting\Helpers\InstanceBuilderUsingModuleConfigurationHelper * @uses \SimpleSAML\Module\accounting\Stores\Connections\Bases\AbstractMigrator * @uses \SimpleSAML\Module\accounting\Services\HelpersManager + * @uses \SimpleSAML\Module\accounting\Stores\Bases\AbstractStore */ class JobsStoreBuilderTest extends TestCase { diff --git a/tests/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store/RawActivityTest.php b/tests/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store/RawActivityTest.php index d0635ea..5c9febc 100644 --- a/tests/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store/RawActivityTest.php +++ b/tests/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store/RawActivityTest.php @@ -73,6 +73,7 @@ class RawActivityTest extends TestCase $rawRow = $this->rawRow; unset($rawRow[TableConstants::ENTITY_ACTIVITY_COLUMN_NAME_CLIENT_IP_ADDRESS]); + /** @psalm-suppress PossiblyInvalidArgument */ $rawActivity = new RawActivity($rawRow, $this->abstractPlatformStub); $this->assertNull($rawActivity->getClientIpAddress()); } diff --git a/tests/src/Stores/Data/Authentication/DoctrineDbal/Versioned/StoreTest.php b/tests/src/Stores/Data/Authentication/DoctrineDbal/Versioned/StoreTest.php index 5cbed48..e1aa07d 100644 --- a/tests/src/Stores/Data/Authentication/DoctrineDbal/Versioned/StoreTest.php +++ b/tests/src/Stores/Data/Authentication/DoctrineDbal/Versioned/StoreTest.php @@ -55,6 +55,7 @@ use SimpleSAML\Test\Module\accounting\Constants\StateArrays; * @uses \SimpleSAML\Module\accounting\Entities\Activity * @uses \SimpleSAML\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned\Store\RawActivity * @uses \SimpleSAML\Module\accounting\Services\HelpersManager + * @uses \SimpleSAML\Module\accounting\Stores\Bases\AbstractStore * * @psalm-suppress all */ @@ -114,7 +115,13 @@ class StoreTest extends TestCase /** @psalm-suppress InvalidArgument */ $this->assertInstanceOf( Store::class, - new Store($this->moduleConfigurationStub, $this->loggerMock, $this->factoryStub) + new Store( + $this->moduleConfigurationStub, + $this->loggerMock, + null, + ModuleConfiguration\ConnectionType::MASTER, + $this->factoryStub + ) ); } @@ -130,7 +137,13 @@ class StoreTest extends TestCase public function testCanPersistAuthenticationEvent(): void { /** @psalm-suppress InvalidArgument */ - $store = new Store($this->moduleConfigurationStub, $this->loggerMock, $this->factoryStub); + $store = new Store( + $this->moduleConfigurationStub, + $this->loggerMock, + null, + ModuleConfiguration\ConnectionType::MASTER, + $this->factoryStub + ); $store->runSetup(); $idpCountQueryBuilder = $this->connection->dbal()->createQueryBuilder(); @@ -232,9 +245,9 @@ class StoreTest extends TestCase $store = new Store( $this->moduleConfigurationStub, $this->loggerMock, - $this->factoryStub, null, ModuleConfiguration\ConnectionType::MASTER, + $this->factoryStub, $this->repositoryStub ); @@ -253,9 +266,9 @@ class StoreTest extends TestCase $store = new Store( $this->moduleConfigurationStub, $this->loggerMock, - $this->factoryStub, null, ModuleConfiguration\ConnectionType::MASTER, + $this->factoryStub, $this->repositoryStub ); @@ -272,9 +285,9 @@ class StoreTest extends TestCase $store = new Store( $this->moduleConfigurationStub, $this->loggerMock, - $this->factoryStub, null, ModuleConfiguration\ConnectionType::MASTER, + $this->factoryStub, $this->repositoryStub ); @@ -293,9 +306,9 @@ class StoreTest extends TestCase $store = new Store( $this->moduleConfigurationStub, $this->loggerMock, - $this->factoryStub, null, ModuleConfiguration\ConnectionType::MASTER, + $this->factoryStub, $this->repositoryStub ); @@ -312,9 +325,9 @@ class StoreTest extends TestCase $store = new Store( $this->moduleConfigurationStub, $this->loggerMock, - $this->factoryStub, null, ModuleConfiguration\ConnectionType::MASTER, + $this->factoryStub, $this->repositoryStub ); @@ -333,9 +346,9 @@ class StoreTest extends TestCase $store = new Store( $this->moduleConfigurationStub, $this->loggerMock, - $this->factoryStub, null, ModuleConfiguration\ConnectionType::MASTER, + $this->factoryStub, $this->repositoryStub ); @@ -352,9 +365,9 @@ class StoreTest extends TestCase $store = new Store( $this->moduleConfigurationStub, $this->loggerMock, - $this->factoryStub, null, ModuleConfiguration\ConnectionType::MASTER, + $this->factoryStub, $this->repositoryStub ); @@ -373,9 +386,9 @@ class StoreTest extends TestCase $store = new Store( $this->moduleConfigurationStub, $this->loggerMock, - $this->factoryStub, null, ModuleConfiguration\ConnectionType::MASTER, + $this->factoryStub, $this->repositoryStub ); @@ -394,9 +407,9 @@ class StoreTest extends TestCase $store = new Store( $moduleConfigurationStub, $this->loggerMock, - $this->factoryStub, null, ModuleConfiguration\ConnectionType::MASTER, + $this->factoryStub, $this->repositoryStub ); @@ -412,9 +425,9 @@ class StoreTest extends TestCase $store = new Store( $this->moduleConfigurationStub, $this->loggerMock, - $this->factoryStub, null, ModuleConfiguration\ConnectionType::MASTER, + $this->factoryStub, $this->repositoryStub ); @@ -433,9 +446,9 @@ class StoreTest extends TestCase $store = new Store( $this->moduleConfigurationStub, $this->loggerMock, - $this->factoryStub, null, ModuleConfiguration\ConnectionType::MASTER, + $this->factoryStub, $this->repositoryStub ); @@ -452,9 +465,9 @@ class StoreTest extends TestCase $store = new Store( $this->moduleConfigurationStub, $this->loggerMock, - $this->factoryStub, null, ModuleConfiguration\ConnectionType::MASTER, + $this->factoryStub, $this->repositoryStub ); @@ -473,9 +486,9 @@ class StoreTest extends TestCase $store = new Store( $this->moduleConfigurationStub, $this->loggerMock, - $this->factoryStub, null, ModuleConfiguration\ConnectionType::MASTER, + $this->factoryStub, $this->repositoryStub ); @@ -492,9 +505,9 @@ class StoreTest extends TestCase $store = new Store( $this->moduleConfigurationStub, $this->loggerMock, - $this->factoryStub, null, ModuleConfiguration\ConnectionType::MASTER, + $this->factoryStub, $this->repositoryStub ); @@ -513,9 +526,9 @@ class StoreTest extends TestCase $store = new Store( $this->moduleConfigurationStub, $this->loggerMock, - $this->factoryStub, null, ModuleConfiguration\ConnectionType::MASTER, + $this->factoryStub, $this->repositoryStub ); @@ -533,9 +546,9 @@ class StoreTest extends TestCase $store = new Store( $this->moduleConfigurationStub, $this->loggerMock, - $this->factoryStub, null, ModuleConfiguration\ConnectionType::MASTER, + $this->factoryStub, $this->repositoryStub ); @@ -553,9 +566,9 @@ class StoreTest extends TestCase $store = new Store( $this->moduleConfigurationStub, $this->loggerMock, - $this->factoryStub, null, ModuleConfiguration\ConnectionType::MASTER, + $this->factoryStub, $this->repositoryStub ); @@ -576,9 +589,9 @@ class StoreTest extends TestCase $store = new Store( $this->moduleConfigurationStub, $this->loggerMock, - $this->factoryStub, null, ModuleConfiguration\ConnectionType::MASTER, + $this->factoryStub, $this->repositoryStub ); @@ -594,9 +607,9 @@ class StoreTest extends TestCase $store = new Store( $this->moduleConfigurationStub, $this->loggerMock, - $this->factoryStub, null, ModuleConfiguration\ConnectionType::MASTER, + $this->factoryStub, $this->repositoryStub ); @@ -614,9 +627,9 @@ class StoreTest extends TestCase $store = new Store( $this->moduleConfigurationStub, $this->loggerMock, - $this->factoryStub, null, ModuleConfiguration\ConnectionType::MASTER, + $this->factoryStub, $this->repositoryStub ); @@ -637,9 +650,9 @@ class StoreTest extends TestCase $store = new Store( $this->moduleConfigurationStub, $this->loggerMock, - $this->factoryStub, null, ModuleConfiguration\ConnectionType::MASTER, + $this->factoryStub, $this->repositoryStub ); diff --git a/tests/src/Stores/Jobs/DoctrineDbal/Store/RepositoryTest.php b/tests/src/Stores/Jobs/DoctrineDbal/Store/RepositoryTest.php index 30a427b..3241e83 100644 --- a/tests/src/Stores/Jobs/DoctrineDbal/Store/RepositoryTest.php +++ b/tests/src/Stores/Jobs/DoctrineDbal/Store/RepositoryTest.php @@ -42,6 +42,7 @@ use SimpleSAML\Test\Module\accounting\Constants\StateArrays; * @uses \SimpleSAML\Module\accounting\Stores\Bases\DoctrineDbal\AbstractRawEntity * @uses \SimpleSAML\Module\accounting\Helpers\NetworkHelper * @uses \SimpleSAML\Module\accounting\Services\HelpersManager + * @uses \SimpleSAML\Module\accounting\Stores\Bases\AbstractStore */ class RepositoryTest extends TestCase { @@ -77,7 +78,13 @@ class RepositoryTest extends TestCase $this->jobStub->method('getCreatedAt')->willReturn(new \DateTimeImmutable()); /** @psalm-suppress InvalidArgument */ - $this->jobsStore = new Store($this->moduleConfiguration, $this->loggerServiceStub, $this->factoryStub); + $this->jobsStore = new Store( + $this->moduleConfiguration, + $this->loggerServiceStub, + null, + ModuleConfiguration\ConnectionType::MASTER, + $this->factoryStub + ); $this->jobsTableName = $this->connection->preparePrefixedTableName(Store\TableConstants::TABLE_NAME_JOB); } diff --git a/tests/src/Stores/Jobs/DoctrineDbal/StoreTest.php b/tests/src/Stores/Jobs/DoctrineDbal/StoreTest.php index 85d78ae..3aca118 100644 --- a/tests/src/Stores/Jobs/DoctrineDbal/StoreTest.php +++ b/tests/src/Stores/Jobs/DoctrineDbal/StoreTest.php @@ -42,6 +42,7 @@ use SimpleSAML\Test\Module\accounting\Constants\StateArrays; * @uses \SimpleSAML\Module\accounting\Stores\Bases\DoctrineDbal\AbstractRawEntity * @uses \SimpleSAML\Module\accounting\Helpers\NetworkHelper * @uses \SimpleSAML\Module\accounting\Services\HelpersManager + * @uses \SimpleSAML\Module\accounting\Stores\Bases\AbstractStore */ class StoreTest extends TestCase { @@ -79,7 +80,13 @@ class StoreTest extends TestCase public function testSetupDependsOnMigratorSetup(): void { /** @psalm-suppress InvalidArgument */ - $jobsStore = new Store($this->moduleConfiguration, $this->loggerStub, $this->factoryStub); + $jobsStore = new Store( + $this->moduleConfiguration, + $this->loggerStub, + null, + ModuleConfiguration\ConnectionType::MASTER, + $this->factoryStub + ); $this->assertTrue($this->migrator->needsSetup()); $this->assertTrue($jobsStore->needsSetup()); @@ -93,7 +100,13 @@ class StoreTest extends TestCase public function testSetupDependsOnMigrations(): void { /** @psalm-suppress InvalidArgument */ - $jobsStore = new Store($this->moduleConfiguration, $this->loggerStub, $this->factoryStub); + $jobsStore = new Store( + $this->moduleConfiguration, + $this->loggerStub, + null, + ModuleConfiguration\ConnectionType::MASTER, + $this->factoryStub + ); // Run migrator setup beforehand, so it only depends on Store migrations setup $this->migrator->runSetup(); @@ -107,7 +120,13 @@ class StoreTest extends TestCase public function testCanGetPrefixedTableNames(): void { /** @psalm-suppress InvalidArgument */ - $jobsStore = new Store($this->moduleConfiguration, $this->loggerStub, $this->factoryStub); + $jobsStore = new Store( + $this->moduleConfiguration, + $this->loggerStub, + null, + ModuleConfiguration\ConnectionType::MASTER, + $this->factoryStub + ); $tableNameJobs = $this->connection->preparePrefixedTableName(Store\TableConstants::TABLE_NAME_JOB); $tableNameFailedJobs = $this->connection->preparePrefixedTableName( @@ -130,7 +149,13 @@ class StoreTest extends TestCase public function testCanEnqueueJob(): void { /** @psalm-suppress InvalidArgument */ - $jobsStore = new Store($this->moduleConfiguration, $this->loggerStub, $this->factoryStub); + $jobsStore = new Store( + $this->moduleConfiguration, + $this->loggerStub, + null, + ModuleConfiguration\ConnectionType::MASTER, + $this->factoryStub + ); $jobsStore->runSetup(); $queryBuilder = $this->connection->dbal()->createQueryBuilder(); @@ -154,7 +179,13 @@ class StoreTest extends TestCase public function testEnqueueThrowsStoreExceptionOnNonSetupRun(): void { /** @psalm-suppress InvalidArgument */ - $jobsStore = new Store($this->moduleConfiguration, $this->loggerStub, $this->factoryStub); + $jobsStore = new Store( + $this->moduleConfiguration, + $this->loggerStub, + null, + ModuleConfiguration\ConnectionType::MASTER, + $this->factoryStub + ); // Don't run setup, so we get exception //$jobsStore->runSetup(); @@ -170,7 +201,13 @@ class StoreTest extends TestCase public function testCanDequeueJob(): void { /** @psalm-suppress InvalidArgument */ - $jobsStore = new Store($this->moduleConfiguration, $this->loggerStub, $this->factoryStub); + $jobsStore = new Store( + $this->moduleConfiguration, + $this->loggerStub, + null, + ModuleConfiguration\ConnectionType::MASTER, + $this->factoryStub + ); $jobsStore->runSetup(); $queryBuilder = $this->connection->dbal()->createQueryBuilder(); @@ -185,7 +222,8 @@ class StoreTest extends TestCase $this->assertSame(2, (int) $queryBuilder->executeQuery()->fetchOne()); - $jobsStore->dequeue(); + /** @psalm-suppress MixedArgument, UndefinedInterfaceMethod */ + $jobsStore->dequeue($this->jobStub->getType()); $this->assertSame(1, (int) $queryBuilder->executeQuery()->fetchOne()); } @@ -193,7 +231,13 @@ class StoreTest extends TestCase public function testCanDequeueSpecificJobType(): void { /** @psalm-suppress InvalidArgument */ - $jobsStore = new Store($this->moduleConfiguration, $this->loggerStub, $this->factoryStub); + $jobsStore = new Store( + $this->moduleConfiguration, + $this->loggerStub, + null, + ModuleConfiguration\ConnectionType::MASTER, + $this->factoryStub + ); $jobsStore->runSetup(); $authenticationEvent = new Event(new State(StateArrays::FULL)); @@ -222,7 +266,13 @@ class StoreTest extends TestCase public function testDequeueThrowsWhenSetupNotRun(): void { /** @psalm-suppress InvalidArgument */ - $jobsStore = new Store($this->moduleConfiguration, $this->loggerStub, $this->factoryStub); + $jobsStore = new Store( + $this->moduleConfiguration, + $this->loggerStub, + null, + ModuleConfiguration\ConnectionType::MASTER, + $this->factoryStub + ); // $jobsStore->runSetup(); $payloadStub = $this->createStub(AbstractPayload::class); @@ -249,16 +299,17 @@ class StoreTest extends TestCase $jobsStore = new Store( $this->moduleConfiguration, $this->loggerStub, - $this->factoryStub, null, ModuleConfiguration\ConnectionType::MASTER, + $this->factoryStub, $repositoryStub ); $jobsStore->runSetup(); $this->expectException(StoreException::class); - $jobsStore->dequeue(); + /** @psalm-suppress MixedArgument, UndefinedInterfaceMethod */ + $jobsStore->dequeue($this->jobStub->getType()); } public function testDequeThrowsAfterMaxDeleteAttempts(): void @@ -271,16 +322,17 @@ class StoreTest extends TestCase $jobsStore = new Store( $this->moduleConfiguration, $this->loggerStub, - $this->factoryStub, null, ModuleConfiguration\ConnectionType::MASTER, + $this->factoryStub, $repositoryStub ); $jobsStore->runSetup(); $this->expectException(StoreException::class); - $jobsStore->dequeue(); + /** @psalm-suppress MixedArgument, UndefinedInterfaceMethod */ + $jobsStore->dequeue($this->jobStub->getType()); } public function testCanContinueSearchingInCaseOfJobDeletion(): void @@ -293,20 +345,27 @@ class StoreTest extends TestCase $jobsStore = new Store( $this->moduleConfiguration, $this->loggerStub, - $this->factoryStub, null, ModuleConfiguration\ConnectionType::MASTER, + $this->factoryStub, $repositoryStub ); $jobsStore->runSetup(); - $this->assertNotNull($jobsStore->dequeue()); + /** @psalm-suppress MixedArgument, UndefinedInterfaceMethod */ + $this->assertNotNull($jobsStore->dequeue($this->jobStub->getType())); } public function testCanMarkFailedJob(): void { /** @psalm-suppress InvalidArgument */ - $jobsStore = new Store($this->moduleConfiguration, $this->loggerStub, $this->factoryStub); + $jobsStore = new Store( + $this->moduleConfiguration, + $this->loggerStub, + null, + ModuleConfiguration\ConnectionType::MASTER, + $this->factoryStub + ); $jobsStore->runSetup(); $queryBuilder = $this->connection->dbal()->createQueryBuilder(); diff --git a/tests/src/Stores/Jobs/PhpRedis/RedisStoreTest.php b/tests/src/Stores/Jobs/PhpRedis/RedisStoreTest.php new file mode 100644 index 0000000..f29ce0c --- /dev/null +++ b/tests/src/Stores/Jobs/PhpRedis/RedisStoreTest.php @@ -0,0 +1,377 @@ +<?php + +declare(strict_types=1); + +namespace SimpleSAML\Test\Module\accounting\Stores\Jobs\PhpRedis; + +use Psr\Log\LoggerInterface; +use Redis; +use SimpleSAML\Module\accounting\Entities\GenericJob; +use SimpleSAML\Module\accounting\Entities\Interfaces\JobInterface; +use SimpleSAML\Module\accounting\Exceptions\InvalidConfigurationException; +use SimpleSAML\Module\accounting\Exceptions\StoreException; +use SimpleSAML\Module\accounting\ModuleConfiguration; +use SimpleSAML\Module\accounting\Stores\Jobs\PhpRedis\RedisStore; +use PHPUnit\Framework\TestCase; + +/** + * @covers \SimpleSAML\Module\accounting\Stores\Jobs\PhpRedis\RedisStore + * @uses \SimpleSAML\Module\accounting\Stores\Bases\AbstractStore + */ +class RedisStoreTest extends TestCase +{ + /** + * @var \PHPUnit\Framework\MockObject\Stub|ModuleConfiguration + */ + protected $moduleConfigurationStub; + /** + * @var \PHPUnit\Framework\MockObject\MockObject|LoggerInterface + */ + protected $loggerMock; + /** + * @var \PHPUnit\Framework\MockObject\MockObject|Redis + */ + protected $redisMock; + /** + * @var \PHPUnit\Framework\MockObject\Stub|JobInterface + */ + protected $jobStub; + + protected function setUp(): void + { + $this->moduleConfigurationStub = $this->createStub(ModuleConfiguration::class); + $this->loggerMock = $this->createMock(LoggerInterface::class); + $this->redisMock = $this->createMock(Redis::class); + $this->jobStub = $this->createStub(JobInterface::class); + $this->jobStub->method('getType')->willReturn(GenericJob::class); + } + + public function testCanCreateInstance(): void + { + /** @psalm-suppress PossiblyUndefinedMethod, MixedMethodCall */ + $this->moduleConfigurationStub->method('getConnectionParameters') + ->willReturn(['host' => 'sample']); + + /** @psalm-suppress PossiblyInvalidArgument */ + $this->assertInstanceOf( + RedisStore::class, + new RedisStore( + $this->moduleConfigurationStub, + $this->loggerMock, + null, + ModuleConfiguration\ConnectionType::MASTER, + $this->redisMock + ) + ); + } + + public function testThrowsIfHostConnectionParameterNotSet(): void + { + $this->expectException(InvalidConfigurationException::class); + + /** @psalm-suppress PossiblyInvalidArgument */ + $this->assertInstanceOf( + RedisStore::class, + new RedisStore( + $this->moduleConfigurationStub, + $this->loggerMock, + null, + ModuleConfiguration\ConnectionType::MASTER, + $this->redisMock + ) + ); + } + + public function testThrowsOnConnectionError(): void + { + $this->expectException(StoreException::class); + + /** @psalm-suppress PossiblyUndefinedMethod, MixedMethodCall */ + $this->loggerMock->expects($this->atLeastOnce()) + ->method('error') + ->with($this->stringContains('Error trying to connect to Redis DB.')); + + /** @psalm-suppress PossiblyUndefinedMethod, MixedMethodCall */ + $this->moduleConfigurationStub->method('getConnectionParameters') + ->willReturn(['host' => 'sample']); + + /** @psalm-suppress PossiblyUndefinedMethod, MixedMethodCall */ + $this->redisMock->method('connect')->willThrowException(new \RedisException('test')); + + /** @psalm-suppress PossiblyInvalidArgument */ + $this->assertInstanceOf( + RedisStore::class, + new RedisStore( + $this->moduleConfigurationStub, + $this->loggerMock, + null, + ModuleConfiguration\ConnectionType::MASTER, + $this->redisMock + ) + ); + } + + public function testThrowsOnAuthError(): void + { + $this->expectException(StoreException::class); + + /** @psalm-suppress PossiblyUndefinedMethod, MixedMethodCall */ + $this->loggerMock->expects($this->atLeastOnce()) + ->method('error') + ->with($this->stringContains('Error trying to set auth parameter for Redis.')); + + /** @psalm-suppress PossiblyUndefinedMethod, MixedMethodCall */ + $this->moduleConfigurationStub->method('getConnectionParameters') + ->willReturn(['host' => 'sample', 'auth' => 'test']); + + /** @psalm-suppress PossiblyUndefinedMethod, MixedMethodCall */ + $this->redisMock->method('auth')->willThrowException(new \RedisException('test')); + + /** @psalm-suppress PossiblyInvalidArgument */ + $this->assertInstanceOf( + RedisStore::class, + new RedisStore( + $this->moduleConfigurationStub, + $this->loggerMock, + null, + ModuleConfiguration\ConnectionType::MASTER, + $this->redisMock + ) + ); + } + + public function testThrowsOnSetPrefixOptionError(): void + { + $this->expectException(StoreException::class); + /** @psalm-suppress PossiblyUndefinedMethod, MixedMethodCall */ + $this->loggerMock->expects($this->atLeastOnce()) + ->method('error') + ->with($this->stringContains('Could not set key prefix for Redis.')); + + /** @psalm-suppress PossiblyUndefinedMethod, MixedMethodCall */ + $this->moduleConfigurationStub->method('getConnectionParameters') + ->willReturn(['host' => 'sample']); + + /** @psalm-suppress PossiblyUndefinedMethod, MixedMethodCall */ + $this->redisMock->method('setOption')->willThrowException(new \RedisException('test')); + + /** @psalm-suppress PossiblyInvalidArgument */ + $this->assertInstanceOf( + RedisStore::class, + new RedisStore( + $this->moduleConfigurationStub, + $this->loggerMock, + null, + ModuleConfiguration\ConnectionType::MASTER, + $this->redisMock + ) + ); + } + + public function testCanCallRPushMethod(): void + { + /** @psalm-suppress PossiblyUndefinedMethod, MixedMethodCall */ + $this->moduleConfigurationStub->method('getConnectionParameters') + ->willReturn(['host' => 'sample']); + + /** @psalm-suppress PossiblyUndefinedMethod, MixedMethodCall */ + $this->redisMock->method('isConnected')->willReturn(true); + /** @psalm-suppress PossiblyUndefinedMethod, MixedMethodCall */ + $this->redisMock->expects($this->once()) + ->method('rPush') + ->with($this->stringStartsWith(RedisStore::LIST_KEY_JOB), $this->isType('string')); + + /** @psalm-suppress PossiblyInvalidArgument */ + $redisStore = new RedisStore( + $this->moduleConfigurationStub, + $this->loggerMock, + null, + ModuleConfiguration\ConnectionType::MASTER, + $this->redisMock + ); + + /** @psalm-suppress PossiblyInvalidArgument */ + $redisStore->enqueue($this->jobStub); + } + + public function testThrowsOnRPushError(): void + { + $this->expectException(StoreException::class); + /** @psalm-suppress PossiblyUndefinedMethod, MixedMethodCall */ + $this->moduleConfigurationStub->method('getConnectionParameters') + ->willReturn(['host' => 'sample']); + /** @psalm-suppress PossiblyUndefinedMethod, MixedMethodCall */ + $this->loggerMock->expects($this->atLeastOnce()) + ->method('error') + ->with($this->stringContains('Could not add job to Redis list.')); + + /** @psalm-suppress PossiblyUndefinedMethod, MixedMethodCall */ + $this->redisMock->method('rPush')->willThrowException(new \RedisException('test')); + + /** @psalm-suppress PossiblyInvalidArgument */ + $redisStore = new RedisStore( + $this->moduleConfigurationStub, + $this->loggerMock, + null, + ModuleConfiguration\ConnectionType::MASTER, + $this->redisMock + ); + + /** @psalm-suppress PossiblyInvalidArgument */ + $redisStore->enqueue($this->jobStub); + } + + public function testCanDequeueJob(): void + { + /** @psalm-suppress PossiblyUndefinedMethod, MixedMethodCall */ + $this->moduleConfigurationStub->method('getConnectionParameters') + ->willReturn(['host' => 'sample']); + + /** @psalm-suppress PossiblyUndefinedMethod, MixedMethodCall */ + $this->redisMock->method('lPop') + ->willReturn(serialize($this->jobStub)); + + /** @psalm-suppress PossiblyInvalidArgument */ + $redisStore = new RedisStore( + $this->moduleConfigurationStub, + $this->loggerMock, + null, + ModuleConfiguration\ConnectionType::MASTER, + $this->redisMock + ); + + /** @psalm-suppress PossiblyInvalidArgument, MixedArgument, PossiblyUndefinedMethod */ + $this->assertInstanceOf(JobInterface::class, $redisStore->dequeue($this->jobStub->getType())); + } + + public function testThrowsOnLPopError(): void + { + /** @psalm-suppress PossiblyUndefinedMethod, MixedMethodCall */ + $this->moduleConfigurationStub->method('getConnectionParameters') + ->willReturn(['host' => 'sample']); + + /** @psalm-suppress PossiblyUndefinedMethod, MixedMethodCall */ + $this->redisMock->method('lPop') + ->willThrowException(new \RedisException('test')); + + $this->expectException(StoreException::class); + /** @psalm-suppress PossiblyUndefinedMethod, MixedMethodCall */ + $this->loggerMock->expects($this->atLeastOnce()) + ->method('error') + ->with($this->stringContains('Could not pop job from Redis list.')); + + /** @psalm-suppress PossiblyInvalidArgument */ + $redisStore = new RedisStore( + $this->moduleConfigurationStub, + $this->loggerMock, + null, + ModuleConfiguration\ConnectionType::MASTER, + $this->redisMock + ); + + /** @psalm-suppress PossiblyInvalidArgument, MixedArgument, PossiblyUndefinedMethod */ + $redisStore->dequeue($this->jobStub->getType()); + } + + public function testThrowsIfNotAbleToDeserializeJobEntry(): void + { + /** @psalm-suppress PossiblyUndefinedMethod, MixedMethodCall */ + $this->moduleConfigurationStub->method('getConnectionParameters') + ->willReturn(['host' => 'sample']); + + /** @psalm-suppress PossiblyUndefinedMethod, MixedMethodCall */ + $this->redisMock->method('lPop') + ->willReturn('invalid'); + + $this->expectException(StoreException::class); + /** @psalm-suppress PossiblyUndefinedMethod, MixedMethodCall */ + $this->loggerMock->expects($this->atLeastOnce()) + ->method('error') + ->with($this->stringContains('Could not deserialize job entry which was available in Redis.')); + + /** @psalm-suppress PossiblyInvalidArgument */ + $redisStore = new RedisStore( + $this->moduleConfigurationStub, + $this->loggerMock, + null, + ModuleConfiguration\ConnectionType::MASTER, + $this->redisMock + ); + + // Suppress notice being raised using @ + /** @psalm-suppress PossiblyInvalidArgument, MixedArgument, PossiblyUndefinedMethod */ + @$redisStore->dequeue($this->jobStub->getType()); + } + + public function testCanMarkFailedJob(): void + { + /** @psalm-suppress PossiblyUndefinedMethod, MixedMethodCall */ + $this->moduleConfigurationStub->method('getConnectionParameters') + ->willReturn(['host' => 'sample']); + + /** @psalm-suppress PossiblyUndefinedMethod, MixedMethodCall */ + $this->redisMock->expects($this->once()) + ->method('rPush') + ->with($this->stringStartsWith(RedisStore::LIST_KEY_JOB_FAILED), $this->isType('string')); + + /** @psalm-suppress PossiblyInvalidArgument */ + $redisStore = new RedisStore( + $this->moduleConfigurationStub, + $this->loggerMock, + null, + ModuleConfiguration\ConnectionType::MASTER, + $this->redisMock + ); + + /** @psalm-suppress PossiblyInvalidArgument */ + $redisStore->markFailedJob($this->jobStub); + } + + public function testThrowsOnMarkingFailedJobError(): void + { + /** @psalm-suppress PossiblyUndefinedMethod, MixedMethodCall */ + $this->moduleConfigurationStub->method('getConnectionParameters') + ->willReturn(['host' => 'sample']); + + /** @psalm-suppress PossiblyUndefinedMethod, MixedMethodCall */ + $this->redisMock->method('rPush') + ->willThrowException(new \RedisException('test')); + + $this->expectException(StoreException::class); + /** @psalm-suppress PossiblyUndefinedMethod, MixedMethodCall */ + $this->loggerMock->expects($this->atLeastOnce()) + ->method('error') + ->with($this->stringContains('Could not mark job as failed.')); + + /** @psalm-suppress PossiblyInvalidArgument */ + $redisStore = new RedisStore( + $this->moduleConfigurationStub, + $this->loggerMock, + null, + ModuleConfiguration\ConnectionType::MASTER, + $this->redisMock + ); + + /** @psalm-suppress PossiblyInvalidArgument */ + $redisStore->markFailedJob($this->jobStub); + } + + public function testSetupIsNotNeeded(): void + { + /** @psalm-suppress PossiblyUndefinedMethod, MixedMethodCall */ + $this->moduleConfigurationStub->method('getConnectionParameters') + ->willReturn(['host' => 'sample']); + + /** @psalm-suppress PossiblyInvalidArgument */ + $redisStore = new RedisStore( + $this->moduleConfigurationStub, + $this->loggerMock, + null, + ModuleConfiguration\ConnectionType::MASTER, + $this->redisMock + ); + + /** @psalm-suppress PossiblyInvalidArgument */ + $this->assertFalse($redisStore->needsSetup()); + } +} diff --git a/tests/src/Trackers/Authentication/DoctrineDbal/Versioned/TrackerTest.php b/tests/src/Trackers/Authentication/DoctrineDbal/Versioned/TrackerTest.php index ba0d031..58ed637 100644 --- a/tests/src/Trackers/Authentication/DoctrineDbal/Versioned/TrackerTest.php +++ b/tests/src/Trackers/Authentication/DoctrineDbal/Versioned/TrackerTest.php @@ -29,6 +29,7 @@ use SimpleSAML\Test\Module\accounting\Constants\ConnectionParameters; * @uses \SimpleSAML\Module\accounting\Helpers\HashHelper * @uses \SimpleSAML\Module\accounting\Services\HelpersManager * @uses \SimpleSAML\Module\accounting\Stores\Connections\Bases\AbstractMigrator + * @uses \SimpleSAML\Module\accounting\Stores\Bases\AbstractStore * * @psalm-suppress all */ -- GitLab