diff --git a/composer.json b/composer.json index a746b29462019d758f722b2bb95f732a2dbba331..d88308a618cf7b54713f84fa21da33ef86d4d2e5 100644 --- a/composer.json +++ b/composer.json @@ -30,6 +30,7 @@ "ext-pdo_mysql": "*", "ext-pdo_sqlite": "*", "doctrine/dbal": "^3", + "psr/log": "^1|^2|^3", "simplesamlphp/composer-module-installer": "^1" }, "require-dev": { diff --git a/src/Controller/Test.php b/src/Controller/Test.php index 1098829918603cf8d2ac0ee624573250ce031860..1ef34ed82e9fcd3689c2e14ffd59648693add681 100644 --- a/src/Controller/Test.php +++ b/src/Controller/Test.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace SimpleSAML\Module\accounting\Controller; +use Exception; use SimpleSAML\Configuration; use SimpleSAML\Locale\Translate; use SimpleSAML\Module\accounting\ModuleConfiguration; @@ -35,7 +36,7 @@ class Test /** * @param Request $request * @return Template - * @throws \Exception + * @throws Exception */ public function test(Request $request): Template { diff --git a/src/Entities/AuthenticationEvent.php b/src/Entities/AuthenticationEvent.php new file mode 100644 index 0000000000000000000000000000000000000000..37b349d876ed9519be0b44b3308743e0734a69e2 --- /dev/null +++ b/src/Entities/AuthenticationEvent.php @@ -0,0 +1,22 @@ +<?php + +declare(strict_types=1); + +namespace SimpleSAML\Module\accounting\Entities; + +use SimpleSAML\Module\accounting\Entities\Bases\AbstractPayload; + +class AuthenticationEvent extends AbstractPayload +{ + protected array $state; + + public function __construct(array $state) + { + $this->state = $state; + } + + public function getState(): array + { + return $this->state; + } +} diff --git a/src/Entities/AuthenticationEvent/Job.php b/src/Entities/AuthenticationEvent/Job.php new file mode 100644 index 0000000000000000000000000000000000000000..51308d6fbba4657e453e3b7b4d80240897d91036 --- /dev/null +++ b/src/Entities/AuthenticationEvent/Job.php @@ -0,0 +1,32 @@ +<?php + +declare(strict_types=1); + +namespace SimpleSAML\Module\accounting\Entities\AuthenticationEvent; + +use SimpleSAML\Module\accounting\Entities\AuthenticationEvent; +use SimpleSAML\Module\accounting\Entities\Bases\AbstractPayload; +use SimpleSAML\Module\accounting\Entities\GenericJob; +use SimpleSAML\Module\accounting\Exceptions\UnexpectedValueException; + +class Job extends GenericJob +{ + public function getPayload(): AuthenticationEvent + { + return $this->validatePayload($this->payload); + } + + public function setPayload(AbstractPayload $payload): void + { + $this->payload = $this->validatePayload($payload); + } + + protected function validatePayload(AbstractPayload $payload): AuthenticationEvent + { + if (! ($payload instanceof AuthenticationEvent)) { + throw new UnexpectedValueException('Job payload must be of type AuthenticationEvent.'); + } + + return $payload; + } +} diff --git a/src/Entities/Bases/AbstractJob.php b/src/Entities/Bases/AbstractJob.php new file mode 100644 index 0000000000000000000000000000000000000000..b2f1f73968f4d3adcf622f5e06406e6e230aae25 --- /dev/null +++ b/src/Entities/Bases/AbstractJob.php @@ -0,0 +1,27 @@ +<?php + +declare(strict_types=1); + +namespace SimpleSAML\Module\accounting\Entities\Bases; + +use SimpleSAML\Module\accounting\Entities\Interfaces\JobInterface; + +abstract class AbstractJob implements JobInterface +{ + protected AbstractPayload $payload; + + public function __construct(AbstractPayload $payload) + { + $this->setPayload($payload); + } + + public function getPayload(): AbstractPayload + { + return $this->payload; + } + + public function setPayload(AbstractPayload $payload): void + { + $this->payload = $payload; + } +} diff --git a/src/Entities/Bases/AbstractPayload.php b/src/Entities/Bases/AbstractPayload.php new file mode 100644 index 0000000000000000000000000000000000000000..f01d96b1b555293602f2fab8a032b5373da24fb0 --- /dev/null +++ b/src/Entities/Bases/AbstractPayload.php @@ -0,0 +1,9 @@ +<?php + +declare(strict_types=1); + +namespace SimpleSAML\Module\accounting\Entities\Bases; + +abstract class AbstractPayload +{ +} diff --git a/src/Entities/GenericJob.php b/src/Entities/GenericJob.php new file mode 100644 index 0000000000000000000000000000000000000000..03aa70051f005bde1e63e11b062d61c9449702f8 --- /dev/null +++ b/src/Entities/GenericJob.php @@ -0,0 +1,7 @@ +<?php + +namespace SimpleSAML\Module\accounting\Entities; + +class GenericJob extends Bases\AbstractJob +{ +} diff --git a/src/Entities/Interfaces/JobInterface.php b/src/Entities/Interfaces/JobInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..c4622c3b09ced9fa1547a945759d6f9bfb7aba6a --- /dev/null +++ b/src/Entities/Interfaces/JobInterface.php @@ -0,0 +1,14 @@ +<?php + +declare(strict_types=1); + +namespace SimpleSAML\Module\accounting\Entities\Interfaces; + +use SimpleSAML\Module\accounting\Entities\Bases\AbstractPayload; + +interface JobInterface +{ + public function getPayload(): AbstractPayload; + + public function setPayload(AbstractPayload $payload): void; +} diff --git a/src/Exceptions/InvalidConfigurationException.php b/src/Exceptions/InvalidConfigurationException.php index 24cf0ebad073769cb93f711c7c3cabfca2d6c85a..034624ec83e27d795f703a7d66b92f57f75cbe99 100644 --- a/src/Exceptions/InvalidConfigurationException.php +++ b/src/Exceptions/InvalidConfigurationException.php @@ -4,6 +4,8 @@ declare(strict_types=1); namespace SimpleSAML\Module\accounting\Exceptions; -class InvalidConfigurationException extends \ValueError +use ValueError; + +class InvalidConfigurationException extends ValueError { } diff --git a/src/Exceptions/InvalidValueException.php b/src/Exceptions/InvalidValueException.php index 81be77ef32ec586e4f48c1eafa3fd6a54e5f9d58..c4932850a481b43fa43d431281074d553bcdaedf 100644 --- a/src/Exceptions/InvalidValueException.php +++ b/src/Exceptions/InvalidValueException.php @@ -4,6 +4,8 @@ declare(strict_types=1); namespace SimpleSAML\Module\accounting\Exceptions; -class InvalidValueException extends \ValueError +use ValueError; + +class InvalidValueException extends ValueError { } diff --git a/src/Exceptions/MigrationException.php b/src/Exceptions/StoreException.php similarity index 61% rename from src/Exceptions/MigrationException.php rename to src/Exceptions/StoreException.php index 5afd8a7d8453c6c54f320690856b1eada1aa7019..3e1b6772e307e438427d0a96a6a942031188a665 100644 --- a/src/Exceptions/MigrationException.php +++ b/src/Exceptions/StoreException.php @@ -4,6 +4,8 @@ declare(strict_types=1); namespace SimpleSAML\Module\accounting\Exceptions; -class MigrationException extends \Exception +use Exception; + +class StoreException extends Exception { } diff --git a/src/Exceptions/StoreException/MigrationException.php b/src/Exceptions/StoreException/MigrationException.php new file mode 100644 index 0000000000000000000000000000000000000000..377d06efc3c13d0e68b9dc5785f4c39dfbc38b91 --- /dev/null +++ b/src/Exceptions/StoreException/MigrationException.php @@ -0,0 +1,11 @@ +<?php + +declare(strict_types=1); + +namespace SimpleSAML\Module\accounting\Exceptions\StoreException; + +use SimpleSAML\Module\accounting\Exceptions\StoreException; + +class MigrationException extends StoreException +{ +} diff --git a/src/Exceptions/UnexpectedValueException.php b/src/Exceptions/UnexpectedValueException.php new file mode 100644 index 0000000000000000000000000000000000000000..22fde53e21b31720a2faf7fb754b41659dd3bd54 --- /dev/null +++ b/src/Exceptions/UnexpectedValueException.php @@ -0,0 +1,9 @@ +<?php + +declare(strict_types=1); + +namespace SimpleSAML\Module\accounting\Exceptions; + +class UnexpectedValueException extends \UnexpectedValueException +{ +} diff --git a/src/Helpers/FilesystemHelper.php b/src/Helpers/FilesystemHelper.php index a33fd0ac1105e0e88627490a407b41e15dcd8d9c..074b461b287011cdfb8308351458811ad66e4b2f 100644 --- a/src/Helpers/FilesystemHelper.php +++ b/src/Helpers/FilesystemHelper.php @@ -10,12 +10,12 @@ class FilesystemHelper { public static function getRealPath(string $path): string { - $path = realpath($path); + $realpath = realpath($path); - if ($path === false || ! (is_dir($path) || is_file($path))) { - throw new InvalidValueException('Given path can not be translated to real path.'); + if ($realpath === false || ! (is_dir($realpath) || is_file($realpath))) { + throw new InvalidValueException(sprintf('Given path can not be translated to real path (%s).', $path)); } - return $path; + return $realpath; } } diff --git a/src/Services/LoggerService.php b/src/Services/LoggerService.php index 5dc711e67a4c2c6d20e68fa0331cce1bb956befd..11827f959d2664fa76a4cc194198ecb8259ca1c3 100644 --- a/src/Services/LoggerService.php +++ b/src/Services/LoggerService.php @@ -1,93 +1,51 @@ <?php +declare(strict_types=1); + namespace SimpleSAML\Module\accounting\Services; +use Psr\Log\AbstractLogger; +use Psr\Log\LogLevel; use SimpleSAML\Logger; -class LoggerService +class LoggerService extends AbstractLogger { - /** - * Log an emergency message. - * - * @param string $message The message to log. - */ - public function emergency(string $message): void - { - Logger::emergency($message); - } - - /** - * Log a critical message. - * - * @param string $message The message to log. - */ - public function critical(string $message): void - { - Logger::critical($message); - } - - /** - * Log an alert message. - * - * @param string $message The message to log. - */ - public function alert(string $message): void - { - Logger::alert($message); - } - - /** - * Log an error message. - * - * @param string $message The message to log. - */ - public function error(string $message): void - { - Logger::error($message); - } - - /** - * Log a warning message. - * - * @param string $message The message to log. - */ - public function warning(string $message): void - { - Logger::warning($message); - } - - /** - * Log a notice message. - * - * @param string $message The message to log. - */ - public function notice(string $message): void - { - Logger::notice($message); - } - - /** - * Log an info message (a bit less verbose than debug messages). - * - * @param string $message The message to log. - */ - public function info(string $message): void - { - Logger::info($message); - } - - /** - * Log a debug message (very verbose messages). - * - * @param string $message The message to log. - */ - public function debug(string $message): void - { - Logger::debug($message); - } - - /** - * Log a statistics message. + public function log($level, $message, array $context = []) + { + if (! empty($context)) { + $message .= ' Context: ' . var_export($context, true); + } + + switch ($level) { + case LogLevel::EMERGENCY: + Logger::emergency($message); + break; + case LogLevel::CRITICAL: + Logger::critical($message); + break; + case LogLevel::ALERT: + Logger::alert($message); + break; + case LogLevel::ERROR: + Logger::error($message); + break; + case LogLevel::WARNING: + Logger::warning($message); + break; + case LogLevel::NOTICE: + Logger::notice($message); + break; + case LogLevel::INFO: + Logger::info($message); + break; + case LogLevel::DEBUG: + Logger::debug($message); + break; + } + } + + /** + * Log an SSP statistics message. * * @param string $message The message to log. */ diff --git a/src/Stores/Connections/Bases/AbstractMigrator.php b/src/Stores/Connections/Bases/AbstractMigrator.php index 0a68d56f216a1236861540eea483b763f3ef43f0..613efc1b6cd5eb47853cbe0471d6b774f54f8ff8 100644 --- a/src/Stores/Connections/Bases/AbstractMigrator.php +++ b/src/Stores/Connections/Bases/AbstractMigrator.php @@ -5,9 +5,10 @@ declare(strict_types=1); namespace SimpleSAML\Module\accounting\Stores\Connections\Bases; use SimpleSAML\Module\accounting\Exceptions\InvalidValueException; -use SimpleSAML\Module\accounting\Exceptions\MigrationException; +use SimpleSAML\Module\accounting\Exceptions\StoreException\MigrationException; use SimpleSAML\Module\accounting\Helpers\FilesystemHelper; use SimpleSAML\Module\accounting\Stores\Interfaces\MigrationInterface; +use Throwable; abstract class AbstractMigrator { @@ -49,6 +50,7 @@ abstract class AbstractMigrator /** * @param class-string[] $migrationClasses * @return void + * @throws MigrationException */ public function runMigrationClasses(array $migrationClasses): void { @@ -59,14 +61,14 @@ abstract class AbstractMigrator try { $migration->run(); - } catch (\Throwable $exception) { + } catch (Throwable $exception) { $message = sprintf( 'Could not run migration class %s. Error was: %s', $migrationClass, $exception->getMessage() ); - throw new MigrationException($message); + throw new MigrationException($message, (int) $exception->getCode(), $exception); } $this->markImplementedMigrationClass($migrationClass); @@ -94,6 +96,9 @@ abstract class AbstractMigrator return ! empty($this->getNonImplementedMigrationClasses($directory, $namespace)); } + /** + * @throws MigrationException + */ public function runNonImplementedMigrationClasses(string $directory, string $namespace): void { $this->runMigrationClasses($this->getNonImplementedMigrationClasses($directory, $namespace)); diff --git a/src/Stores/Connections/DoctrineDbal/Bases/AbstractMigration.php b/src/Stores/Connections/DoctrineDbal/Bases/AbstractMigration.php index f64b7113690f1372ca2ef8ca9ec9ba9d5a4bee20..d0a8b9497ae5850509ed78b5659291a3e4b98c99 100644 --- a/src/Stores/Connections/DoctrineDbal/Bases/AbstractMigration.php +++ b/src/Stores/Connections/DoctrineDbal/Bases/AbstractMigration.php @@ -1,19 +1,47 @@ <?php +declare(strict_types=1); + namespace SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Bases; use Doctrine\DBAL\Schema\AbstractSchemaManager; +use SimpleSAML\Module\accounting\Exceptions\StoreException; +use SimpleSAML\Module\accounting\Exceptions\StoreException\MigrationException; use SimpleSAML\Module\accounting\Stores\Interfaces\MigrationInterface; use SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Connection; +use Throwable; abstract class AbstractMigration implements MigrationInterface { protected Connection $connection; protected AbstractSchemaManager $schemaManager; + /** + * @throws StoreException + */ public function __construct(Connection $connection) { $this->connection = $connection; - $this->schemaManager = $this->connection->dbal()->createSchemaManager(); + try { + $this->schemaManager = $this->connection->dbal()->createSchemaManager(); + } catch (Throwable $exception) { + $message = 'Could not create DBAL schema manager.'; + throw new StoreException($message, (int) $exception->getCode(), $exception); + } + } + + /** + * @throws MigrationException + */ + protected function throwGenericMigrationException(string $contextDetails, Throwable $throwable): void + { + $message = sprintf( + 'There was an error running a migration class %s. Context details: %s. Error was: %s.', + static::class, + $contextDetails, + $throwable->getMessage() + ); + + throw new MigrationException($message, (int) $throwable->getCode(), $throwable); } } diff --git a/src/Stores/Connections/DoctrineDbal/Factory.php b/src/Stores/Connections/DoctrineDbal/Factory.php index 6c2af906f32431080113f9235d128c734de93eaf..45d9c1076bb93e4ac78425ded9c9bfcb57ab48f6 100644 --- a/src/Stores/Connections/DoctrineDbal/Factory.php +++ b/src/Stores/Connections/DoctrineDbal/Factory.php @@ -1,16 +1,18 @@ <?php +declare(strict_types=1); + namespace SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal; +use Psr\Log\LoggerInterface; use SimpleSAML\Module\accounting\ModuleConfiguration; -use SimpleSAML\Module\accounting\Services\LoggerService; class Factory { protected ModuleConfiguration $moduleConfiguration; - protected LoggerService $loggerService; + protected LoggerInterface $loggerService; - public function __construct(ModuleConfiguration $moduleConfiguration, LoggerService $loggerService) + public function __construct(ModuleConfiguration $moduleConfiguration, LoggerInterface $loggerService) { $this->moduleConfiguration = $moduleConfiguration; $this->loggerService = $loggerService; @@ -21,7 +23,7 @@ class Factory return new Connection($this->moduleConfiguration->getStoreConnectionParameters($connectionKey)); } - public function buildMigrator(Connection $connection, LoggerService $loggerService = null): Migrator + public function buildMigrator(Connection $connection, LoggerInterface $loggerService = null): Migrator { return new Migrator($connection, $loggerService ?? $this->loggerService); } diff --git a/src/Stores/Connections/DoctrineDbal/Migrator.php b/src/Stores/Connections/DoctrineDbal/Migrator.php index 39861c7d6875690af664c705420acdd2431ff63d..e804b2876a76aa72d2e1e11d07361be25ede7c9d 100644 --- a/src/Stores/Connections/DoctrineDbal/Migrator.php +++ b/src/Stores/Connections/DoctrineDbal/Migrator.php @@ -1,15 +1,22 @@ <?php +declare(strict_types=1); + namespace SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal; +use DateTimeImmutable; use Doctrine\DBAL\Schema\AbstractSchemaManager; use Doctrine\DBAL\Schema\Table; use Doctrine\DBAL\Types\Types; +use Psr\Log\LoggerInterface; +use ReflectionClass; +use ReflectionException; use SimpleSAML\Module\accounting\Exceptions\InvalidValueException; -use SimpleSAML\Module\accounting\Services\LoggerService; +use SimpleSAML\Module\accounting\Exceptions\StoreException; use SimpleSAML\Module\accounting\Stores\Connections\Bases\AbstractMigrator; use SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Bases\AbstractMigration; use SimpleSAML\Module\accounting\Stores\Interfaces\MigrationInterface; +use Throwable; class Migrator extends AbstractMigrator { @@ -20,57 +27,87 @@ class Migrator extends AbstractMigrator public const COLUMN_NAME_CREATED_AT = 'created_at'; protected Connection $connection; - protected LoggerService $loggerService; + protected LoggerInterface $logger; protected AbstractSchemaManager $schemaManager; protected string $prefixedTableName; - public function __construct(Connection $connection, LoggerService $loggerService) + /** + * @throws StoreException + */ + public function __construct(Connection $connection, LoggerInterface $logger) { $this->connection = $connection; - $this->loggerService = $loggerService; + $this->logger = $logger; - $this->schemaManager = $this->connection->dbal()->createSchemaManager(); + try { + $this->schemaManager = $this->connection->dbal()->createSchemaManager(); + } catch (Throwable $exception) { + $message = 'Could not create DBAL schema manager.'; + throw new StoreException($message, (int) $exception->getCode(), $exception); + } $this->prefixedTableName = $this->connection->preparePrefixedTableName(self::TABLE_NAME); } + /** + * @throws StoreException + */ public function needsSetup(): bool { - return ! $this->schemaManager->tablesExist([$this->prefixedTableName]); + try { + return ! $this->schemaManager->tablesExist([$this->prefixedTableName]); + } catch (Throwable $exception) { + $message = sprintf('Could not check table %s existence using schema manager.', $this->prefixedTableName); + throw new StoreException($message, (int) $exception->getCode(), $exception); + } } + /** + * @throws StoreException + */ public function runSetup(): void { if (! $this->needsSetup()) { - $this->loggerService->warning('Migrator setup has been called, however setup is not needed.'); + $this->logger->warning('Migrator setup has been called, however setup is not needed.'); return; } $this->createMigrationsTable(); } + /** + * @throws StoreException + */ protected function createMigrationsTable(): void { - $table = new Table($this->prefixedTableName); - - $table->addColumn(self::COLUMN_NAME_ID, Types::BIGINT) - ->setAutoincrement(true) - ->setUnsigned(true); - $table->addColumn(self::COLUMN_NAME_VERSION, Types::STRING); - $table->addColumn(self::COLUMN_NAME_CREATED_AT, Types::DATETIMETZ_IMMUTABLE); - - $table->setPrimaryKey(['id']); - $table->addUniqueIndex([self::COLUMN_NAME_VERSION]); - - $this->schemaManager->createTable($table); + try { + $table = new Table($this->prefixedTableName); + + $table->addColumn(self::COLUMN_NAME_ID, Types::BIGINT) + ->setAutoincrement(true) + ->setUnsigned(true); + $table->addColumn(self::COLUMN_NAME_VERSION, Types::STRING); + $table->addColumn(self::COLUMN_NAME_CREATED_AT, Types::DATETIMETZ_IMMUTABLE); + + $table->setPrimaryKey(['id']); + $table->addUniqueIndex([self::COLUMN_NAME_VERSION]); + + $this->schemaManager->createTable($table); + } catch (Throwable $exception) { + $message = sprintf('Error creating migrations table %s.', $this->prefixedTableName); + throw new StoreException($message, (int) $exception->getCode(), $exception); + } } + /** + * @throws ReflectionException + */ protected function buildMigrationClassInstance(string $migrationClass): MigrationInterface { $this->validateDoctrineDbalMigrationClass($migrationClass); /** @var MigrationInterface $migration */ - $migration = (new \ReflectionClass($migrationClass))->newInstance($this->connection); + $migration = (new ReflectionClass($migrationClass))->newInstance($this->connection); return $migration; } @@ -82,40 +119,56 @@ class Migrator extends AbstractMigrator } } + /** + * @throws StoreException + */ protected function markImplementedMigrationClass(string $migrationClass): void { $queryBuilder = $this->connection->dbal()->createQueryBuilder(); - $queryBuilder->insert($this->prefixedTableName) - ->values( - [ - self::COLUMN_NAME_VERSION => ':' . self::COLUMN_NAME_VERSION, - self::COLUMN_NAME_CREATED_AT => ':' . self::COLUMN_NAME_CREATED_AT, - ] - ) - ->setParameters( - [ - self::COLUMN_NAME_VERSION => $migrationClass, - self::COLUMN_NAME_CREATED_AT => new \DateTimeImmutable(), - ], - [ - self::COLUMN_NAME_VERSION => Types::STRING, - self::COLUMN_NAME_CREATED_AT => Types::DATETIMETZ_IMMUTABLE - ] - ); - - $queryBuilder->executeStatement(); + try { + $queryBuilder->insert($this->prefixedTableName) + ->values( + [ + self::COLUMN_NAME_VERSION => ':' . self::COLUMN_NAME_VERSION, + self::COLUMN_NAME_CREATED_AT => ':' . self::COLUMN_NAME_CREATED_AT, + ] + ) + ->setParameters( + [ + self::COLUMN_NAME_VERSION => $migrationClass, + self::COLUMN_NAME_CREATED_AT => new DateTimeImmutable(), + ], + [ + self::COLUMN_NAME_VERSION => Types::STRING, + self::COLUMN_NAME_CREATED_AT => Types::DATETIMETZ_IMMUTABLE + ] + ); + + $queryBuilder->executeStatement(); + } catch (Throwable $exception) { + $message = sprintf('Error marking implemented migrations class %s.', $migrationClass); + throw new StoreException($message, (int) $exception->getCode(), $exception); + } } + /** + * @throws StoreException + */ public function getImplementedMigrationClasses(): array { - $queryBuilder = $this->connection->dbal()->createQueryBuilder(); + try { + $queryBuilder = $this->connection->dbal()->createQueryBuilder(); - $queryBuilder->select(self::COLUMN_NAME_VERSION) - ->from($this->prefixedTableName); + $queryBuilder->select(self::COLUMN_NAME_VERSION) + ->from($this->prefixedTableName); - /** @var class-string[] $migrationClasses */ - $migrationClasses = $queryBuilder->executeQuery()->fetchFirstColumn(); + /** @var class-string[] $migrationClasses */ + $migrationClasses = $queryBuilder->executeQuery()->fetchFirstColumn(); + } catch (Throwable $exception) { + $message = 'Error getting implemented migration classes.'; + throw new StoreException($message, (int) $exception->getCode(), $exception); + } return $migrationClasses; } diff --git a/src/Stores/Interfaces/ConnectionInterface.php b/src/Stores/Interfaces/ConnectionInterface.php index 359c817b5dfd45cfab9ada2daeddc0cc0801bc61..b61d3208388cd61c501b2df5d93530628f501f59 100644 --- a/src/Stores/Interfaces/ConnectionInterface.php +++ b/src/Stores/Interfaces/ConnectionInterface.php @@ -1,5 +1,7 @@ <?php +declare(strict_types=1); + namespace SimpleSAML\Module\accounting\Stores\Interfaces; interface ConnectionInterface diff --git a/src/Stores/Interfaces/DataStoreInterface.php b/src/Stores/Interfaces/DataStoreInterface.php index b091acf1197ead3a12f744764e8a984b9f31f911..4d06925cd631780d351bc7eca618522d7829c726 100644 --- a/src/Stores/Interfaces/DataStoreInterface.php +++ b/src/Stores/Interfaces/DataStoreInterface.php @@ -1,5 +1,7 @@ <?php +declare(strict_types=1); + namespace SimpleSAML\Module\accounting\Stores\Interfaces; interface DataStoreInterface extends StoreInterface diff --git a/src/Stores/Interfaces/JobsStoreInterface.php b/src/Stores/Interfaces/JobsStoreInterface.php index 3dd0b57bf05c7bc0aec7c69250f25564af415ec0..52d973bb7f08d768cc9650553059148e63f7010f 100644 --- a/src/Stores/Interfaces/JobsStoreInterface.php +++ b/src/Stores/Interfaces/JobsStoreInterface.php @@ -1,7 +1,25 @@ <?php +declare(strict_types=1); + namespace SimpleSAML\Module\accounting\Stores\Interfaces; +use SimpleSAML\Module\accounting\Entities\Interfaces\JobInterface; + interface JobsStoreInterface extends StoreInterface { + /** + * Add job to queue + * @param JobInterface $job + * @param string|null $type Optional job type designation. If not null, payload class FQN will be used. + * @return void + */ + public function enqueue(JobInterface $job, string $type = null): void; + + /** + * Get job from queue + * @param string|null $type + * @return ?JobInterface + */ + public function dequeue(string $type = null): ?JobInterface; } diff --git a/src/Stores/Interfaces/MigrationInterface.php b/src/Stores/Interfaces/MigrationInterface.php index ea52b88a626c4d4c69a2372e0e7158b87d989af9..ce7e523381d6e84b95720bc319baaa8e380cf12e 100644 --- a/src/Stores/Interfaces/MigrationInterface.php +++ b/src/Stores/Interfaces/MigrationInterface.php @@ -1,5 +1,7 @@ <?php +declare(strict_types=1); + namespace SimpleSAML\Module\accounting\Stores\Interfaces; interface MigrationInterface diff --git a/src/Stores/Interfaces/StoreInterface.php b/src/Stores/Interfaces/StoreInterface.php index 082ee53080fce20782c10fd04a26243b1080f292..ea80efa40131b993caac696176123e5a7d1544a4 100644 --- a/src/Stores/Interfaces/StoreInterface.php +++ b/src/Stores/Interfaces/StoreInterface.php @@ -1,5 +1,7 @@ <?php +declare(strict_types=1); + namespace SimpleSAML\Module\accounting\Stores\Interfaces; interface StoreInterface diff --git a/src/Stores/Jobs/DoctrineDbal/JobsStore.php b/src/Stores/Jobs/DoctrineDbal/JobsStore.php index 25590c8883b91bb05c460a5164277bb562ea951d..30f760d5bf4f352a6925c02a5d4881276f262dcf 100644 --- a/src/Stores/Jobs/DoctrineDbal/JobsStore.php +++ b/src/Stores/Jobs/DoctrineDbal/JobsStore.php @@ -4,37 +4,249 @@ declare(strict_types=1); namespace SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal; +use DateTimeImmutable; +use Doctrine\DBAL\Result; +use Doctrine\DBAL\Types\Types; +use Psr\Log\LoggerInterface; +use ReflectionClass; +use SimpleSAML\Module\accounting\Entities\GenericJob; +use SimpleSAML\Module\accounting\Entities\Interfaces\JobInterface; +use SimpleSAML\Module\accounting\Exceptions\StoreException; +use SimpleSAML\Module\accounting\Exceptions\StoreException\MigrationException; use SimpleSAML\Module\accounting\ModuleConfiguration; -use SimpleSAML\Module\accounting\Stores\Jobs\Interfaces\JobsStoreInterface; +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; +use SimpleSAML\Module\accounting\Stores\Interfaces\JobsStoreInterface; +use Throwable; class JobsStore implements JobsStoreInterface { public const TABLE_NAME_JOBS = 'jobs'; public const TABLE_NAME_FAILED_JOBS = 'failed_jobs'; + public const COLUMN_NAME_ID = 'id'; + public const COLUMN_NAME_PAYLOAD = 'payload'; + public const COLUMN_NAME_TYPE = 'type'; + public const COLUMN_NAME_CREATED_AT = 'created_at'; + + public const COLUMN_LENGTH_TYPE = 1024; + protected ModuleConfiguration $moduleConfiguration; protected Connection $connection; protected string $prefixedTableNameJobs; protected string $prefixedTableNameFailedJobs; protected Migrator $migrator; + protected LoggerInterface $logger; - public function __construct(ModuleConfiguration $moduleConfiguration, Factory $factory) - { + public function __construct( + ModuleConfiguration $moduleConfiguration, + Factory $factory, + LoggerInterface $logger + ) { $this->moduleConfiguration = $moduleConfiguration; $this->connection = $factory->buildConnection($moduleConfiguration->getStoreConnection(self::class)); $this->migrator = $factory->buildMigrator($this->connection); + $this->logger = $logger; $this->prefixedTableNameJobs = $this->connection->preparePrefixedTableName(self::TABLE_NAME_JOBS); $this->prefixedTableNameFailedJobs = $this->connection->preparePrefixedTableName(self::TABLE_NAME_FAILED_JOBS); } - public function needsSetUp(): bool + /** + * @throws StoreException + */ + public function enqueue(JobInterface $job, string $type = null): void + { + $queryBuilder = $this->connection->dbal()->createQueryBuilder(); + + $payload = $job->getPayload(); + $type = $this->validateType($type ?? get_class($job)); + + $queryBuilder->insert($this->prefixedTableNameJobs) + ->values( + [ + self::COLUMN_NAME_PAYLOAD => ':' . self::COLUMN_NAME_PAYLOAD, + self::COLUMN_NAME_TYPE => ':' . self::COLUMN_NAME_TYPE, + self::COLUMN_NAME_CREATED_AT => ':' . self::COLUMN_NAME_CREATED_AT, + ] + ) + ->setParameters( + [ + self::COLUMN_NAME_PAYLOAD => serialize($payload), + self::COLUMN_NAME_TYPE => $type, + self::COLUMN_NAME_CREATED_AT => new DateTimeImmutable(), + ], + [ + self::COLUMN_NAME_PAYLOAD => Types::TEXT, + self::COLUMN_NAME_TYPE => Types::STRING, + self::COLUMN_NAME_CREATED_AT => Types::DATETIMETZ_IMMUTABLE + ] + ); + + try { + $queryBuilder->executeStatement(); + } catch (Throwable $exception) { + $message = sprintf('Could not enqueue job (%s)', $exception->getMessage()); + throw new StoreException($message, (int)$exception->getCode(), $exception); + } + } + + /** + * @throws StoreException + */ + protected function validateType(string $type): string + { + if (mb_strlen($type) > self::COLUMN_LENGTH_TYPE) { + throw new StoreException( + sprintf('String length for type column exceeds %s limit.', self::COLUMN_LENGTH_TYPE) + ); + } + + return $type; + } + + /** + * @throws StoreException + */ + public function dequeue(string $type = null): ?JobInterface + { + $job = null; + + try { + // Check if there are any jobs in the store... + while ($row = $this->getNext($type)->fetchAssociative()) { + // We have a row. Let's create a row job instance, which will take care of validation and make it easier + // to work with instead of array. + $rawJob = new RawJob($row, $this->connection->dbal()->getDatabasePlatform()); + // Let's try to delete this job from the store, so it can't be fetched again. + $numberOfAffectedRows = $this->delete($rawJob->getId()); + if ($numberOfAffectedRows === 0) { + // It seems that this job has already been dequeued in the meantime. Try to get next job again. + continue; + } + // Job is deleted, meaning it is now dequeued and we can return a new job instance. + // If a valid type is declared, let's try to instantiate a job of that specific type. + if ($type !== null && class_exists($type) && is_subclass_of($type, JobInterface::class)) { + $job = (new ReflectionClass($type))->newInstance($rawJob->getPayload()); + } else { + // No (valid) job type, so generic job will do... + $job = new GenericJob($rawJob->getPayload()); + } + + // We have found and dequeued a job, so finish with the search. + break; + } + } catch (Throwable $exception) { + throw new StoreException( + 'Error while trying to dequeue a job.', + (int) $exception->getCode(), + $exception + ); + } + + return $job; + } + + /** + * @throws StoreException + */ + public function getNext(string $type = null): Result + { + $queryBuilder = $this->connection->dbal()->createQueryBuilder(); + + /** + * @psalm-suppress TooManyArguments - providing array or null is deprecated + */ + $queryBuilder->select( + self::COLUMN_NAME_ID, + self::COLUMN_NAME_PAYLOAD, + self::COLUMN_NAME_TYPE, + self::COLUMN_NAME_CREATED_AT + ) + ->from($this->prefixedTableNameJobs) + ->orderBy(self::COLUMN_NAME_ID) + ->setMaxResults(1); + + if ($type !== null) { + $queryBuilder->where(self::COLUMN_NAME_TYPE . ' = ' . $queryBuilder->createNamedParameter($type)); + } + + try { + $result = $queryBuilder->executeQuery(); + } catch (Throwable $exception) { + $message = 'Error while trying to execute query to get next available job.'; + throw new StoreException($message, (int)$exception->getCode(), $exception); + } + + return $result; + } + + protected function delete(int $id): int + { + return (int)$this->connection->dbal() + ->delete( + $this->prefixedTableNameJobs, + [self::COLUMN_NAME_ID => $id], + [self::COLUMN_NAME_ID => Types::BIGINT] + ); + } + + /** + * @throws StoreException + * @throws MigrationException + */ + public function runSetup(): void + { + if ($this->migrator->needsSetup()) { + $this->migrator->runSetup(); + } + + if (!$this->areAllMigrationsImplemented()) { + $this->migrator->runNonImplementedMigrationClasses( + $this->getMigrationsDirectory(), + $this->getMigrationsNamespace() + ); + } + } + + /** + * @throws StoreException + */ + public function needsSetup(): bool + { + // ... if the migrator itself needs setup. + if ($this->migrator->needsSetup()) { + return true; + } + + // ... if JobsStore migrations need to run + if (!$this->areAllMigrationsImplemented()) { + return true; + } + + return false; + } + + public function areAllMigrationsImplemented(): bool + { + return !$this->migrator->hasNonImplementedMigrationClasses( + $this->getMigrationsDirectory(), + $this->getMigrationsNamespace() + ); + } + + protected function getMigrationsDirectory(): string + { + return __DIR__ . DIRECTORY_SEPARATOR . + (new ReflectionClass($this))->getShortName() . DIRECTORY_SEPARATOR . + AbstractMigrator::DEFAULT_MIGRATIONS_DIRECTORY_NAME; + } + + protected function getMigrationsNamespace(): string { - // TODO mivanci dovrši - return true; + return self::class . '\\' . AbstractMigrator::DEFAULT_MIGRATIONS_DIRECTORY_NAME; } public function getPrefixedTableNameJobs(): string diff --git a/src/Stores/Jobs/DoctrineDbal/JobsStore/Migrations/Version20220601000000CreateJobsTable.php b/src/Stores/Jobs/DoctrineDbal/JobsStore/Migrations/Version20220601000000CreateJobsTable.php index b0e7581ee8212e5717f74b09b337bfadc9f3e060..d807e673e5040ff7b150f7d0d6ed782c4f49f3bf 100644 --- a/src/Stores/Jobs/DoctrineDbal/JobsStore/Migrations/Version20220601000000CreateJobsTable.php +++ b/src/Stores/Jobs/DoctrineDbal/JobsStore/Migrations/Version20220601000000CreateJobsTable.php @@ -1,37 +1,59 @@ <?php +declare(strict_types=1); + namespace SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\JobsStore\Migrations; use Doctrine\DBAL\Schema\Table; use Doctrine\DBAL\Types\Types; +use SimpleSAML\Module\accounting\Exceptions\StoreException\MigrationException; use SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Bases\AbstractMigration; use SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\JobsStore; +use Throwable; class Version20220601000000CreateJobsTable extends AbstractMigration { + /** + * @throws MigrationException + */ public function run(): void { $tableName = $this->connection->preparePrefixedTableName(JobsStore::TABLE_NAME_JOBS); - $table = new Table($tableName); - $table->addColumn('id', Types::BIGINT) - ->setUnsigned(true) - ->setAutoincrement(true); + try { + $table = new Table($tableName); + + $table->addColumn(JobsStore::COLUMN_NAME_ID, Types::BIGINT) + ->setUnsigned(true) + ->setAutoincrement(true); + + $table->addColumn(JobsStore::COLUMN_NAME_TYPE, Types::STRING) + ->setLength(JobsStore::COLUMN_LENGTH_TYPE); - $table->addColumn('payload', Types::TEXT); - $table->addColumn('created_at', Types::DATETIMETZ_IMMUTABLE); - $table->addColumn('reserved_at', Types::DATETIMETZ_IMMUTABLE)->setNotnull(false); - $table->addColumn('attempts', Types::INTEGER)->setNotnull(false); + $table->addColumn(JobsStore::COLUMN_NAME_PAYLOAD, Types::TEXT); + $table->addColumn(JobsStore::COLUMN_NAME_CREATED_AT, Types::DATETIMETZ_IMMUTABLE); - $table->setPrimaryKey(['id']); + $table->setPrimaryKey([JobsStore::COLUMN_NAME_ID]); - $this->schemaManager->createTable($table); + $this->schemaManager->createTable($table); + } catch (Throwable $exception) { + $contextDetails = sprintf('Could not create table %s.', $tableName); + $this->throwGenericMigrationException($contextDetails, $exception); + } } + /** + * @throws MigrationException + */ public function revert(): void { $tableName = $this->connection->preparePrefixedTableName(JobsStore::TABLE_NAME_JOBS); - $this->schemaManager->dropTable($tableName); + try { + $this->schemaManager->dropTable($tableName); + } catch (Throwable $exception) { + $contextDetails = sprintf('Could not drop table %s.', $tableName); + $this->throwGenericMigrationException($contextDetails, $exception); + } } } diff --git a/src/Stores/Jobs/DoctrineDbal/JobsStore/Migrations/Version20220601000100CreateFailedJobsTable.php b/src/Stores/Jobs/DoctrineDbal/JobsStore/Migrations/Version20220601000100CreateFailedJobsTable.php index e965025d763da8eda7fbc875b8bf7f9403080844..08a39ba94f094c879a8208211643757ab9df1acb 100644 --- a/src/Stores/Jobs/DoctrineDbal/JobsStore/Migrations/Version20220601000100CreateFailedJobsTable.php +++ b/src/Stores/Jobs/DoctrineDbal/JobsStore/Migrations/Version20220601000100CreateFailedJobsTable.php @@ -1,35 +1,55 @@ <?php +declare(strict_types=1); + namespace SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\JobsStore\Migrations; use Doctrine\DBAL\Schema\Table; use Doctrine\DBAL\Types\Types; +use SimpleSAML\Module\accounting\Exceptions\StoreException\MigrationException; use SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Bases\AbstractMigration; use SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\JobsStore; +use Throwable; class Version20220601000100CreateFailedJobsTable extends AbstractMigration { + /** + * @throws MigrationException + */ public function run(): void { $tableName = $this->connection->preparePrefixedTableName(JobsStore::TABLE_NAME_FAILED_JOBS); - $table = new Table($tableName); + try { + $table = new Table($tableName); - $table->addColumn('id', Types::BIGINT) - ->setUnsigned(true) - ->setAutoincrement(true); + $table->addColumn('id', Types::BIGINT) + ->setUnsigned(true) + ->setAutoincrement(true); - $table->addColumn('payload', Types::TEXT); - $table->addColumn('created_at', Types::DATETIMETZ_IMMUTABLE); + $table->addColumn('payload', Types::TEXT); + $table->addColumn('created_at', Types::DATETIMETZ_IMMUTABLE); - $table->setPrimaryKey(['id']); + $table->setPrimaryKey(['id']); - $this->schemaManager->createTable($table); + $this->schemaManager->createTable($table); + } catch (Throwable $exception) { + $contextDetails = sprintf('Could not create table %s.', $tableName); + $this->throwGenericMigrationException($contextDetails, $exception); + } } + /** + * @throws MigrationException + */ public function revert(): void { $tableName = $this->connection->preparePrefixedTableName(JobsStore::TABLE_NAME_FAILED_JOBS); - $this->schemaManager->dropTable($tableName); + try { + $this->schemaManager->dropTable($tableName); + } catch (Throwable $exception) { + $contextDetails = sprintf('Could not drop table %s.', $tableName); + $this->throwGenericMigrationException($contextDetails, $exception); + } } } diff --git a/src/Stores/Jobs/DoctrineDbal/RawJob.php b/src/Stores/Jobs/DoctrineDbal/RawJob.php new file mode 100644 index 0000000000000000000000000000000000000000..e66eb263925311c1331a20894cf8e331d6f18542 --- /dev/null +++ b/src/Stores/Jobs/DoctrineDbal/RawJob.php @@ -0,0 +1,129 @@ +<?php + +declare(strict_types=1); + +namespace SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal; + +use Doctrine\DBAL\Platforms\AbstractPlatform; +use Doctrine\DBAL\Types\Type; +use Doctrine\DBAL\Types\Types; +use SimpleSAML\Module\accounting\Entities\Bases\AbstractPayload; +use SimpleSAML\Module\accounting\Exceptions\UnexpectedValueException; + +class RawJob +{ + protected int $id; + protected AbstractPayload $payload; + protected string $type; + protected \DateTimeImmutable $createdAt; + protected AbstractPlatform $abstractPlatform; + + public function __construct(array $rawRow, AbstractPlatform $abstractPlatform) + { + $this->abstractPlatform = $abstractPlatform; + + $this->validate($rawRow); + + $this->id = (int)$rawRow[JobsStore::COLUMN_NAME_ID]; + $this->payload = $this->resolvePayload((string)$rawRow[JobsStore::COLUMN_NAME_PAYLOAD]); + $this->type = (string)$rawRow[JobsStore::COLUMN_NAME_TYPE]; + $this->createdAt = $this->resolveCreatedAt($rawRow); + } + + protected function validate(array $rawRow): void + { + $columnsToCheck = [ + JobsStore::COLUMN_NAME_ID, + JobsStore::COLUMN_NAME_PAYLOAD, + JobsStore::COLUMN_NAME_TYPE, + JobsStore::COLUMN_NAME_CREATED_AT, + ]; + + foreach ($columnsToCheck as $column) { + if (empty($rawRow[$column])) { + throw new UnexpectedValueException(sprintf('Column %s must be set.', $column)); + } + } + + if (! is_numeric($rawRow[JobsStore::COLUMN_NAME_ID])) { + throw new UnexpectedValueException(sprintf('Column %s must be numeric.', JobsStore::COLUMN_NAME_ID)); + } + + if (! is_string($rawRow[JobsStore::COLUMN_NAME_PAYLOAD])) { + throw new UnexpectedValueException(sprintf('Column %s must be string.', JobsStore::COLUMN_NAME_PAYLOAD)); + } + + if (! is_string($rawRow[JobsStore::COLUMN_NAME_TYPE])) { + throw new UnexpectedValueException(sprintf('Column %s must be string.', JobsStore::COLUMN_NAME_TYPE)); + } + + if (! is_string($rawRow[JobsStore::COLUMN_NAME_CREATED_AT])) { + throw new UnexpectedValueException(sprintf('Column %s must be string.', JobsStore::COLUMN_NAME_CREATED_AT)); + } + } + + protected function resolvePayload(string $rawPayload): AbstractPayload + { + /** @psalm-suppress MixedAssignment - we check the type manually */ + $payload = unserialize($rawPayload); + + if ($payload instanceof AbstractPayload) { + return $payload; + } + + throw new UnexpectedValueException('Job payload is not instance of AbstractPayload.'); + } + + /** + * @return int + */ + public function getId(): int + { + return $this->id; + } + + /** + * @return AbstractPayload + */ + public function getPayload(): AbstractPayload + { + return $this->payload; + } + + /** + * @return string + */ + public function getType(): string + { + return $this->type; + } + + protected function resolveCreatedAt(array $rawRow): \DateTimeImmutable + { + try { + /** @var \DateTimeImmutable $createdAt */ + $createdAt = (Type::getType(Types::DATETIME_IMMUTABLE)) + ->convertToPHPValue($rawRow[JobsStore::COLUMN_NAME_CREATED_AT], $this->abstractPlatform); + } catch (\Throwable $exception) { + throw new UnexpectedValueException( + sprintf( + 'Could not create instance of DateTimeImmutable using value %s for column %s.', + var_export($rawRow[JobsStore::COLUMN_NAME_CREATED_AT], true), + JobsStore::COLUMN_NAME_CREATED_AT + ), + (int)$exception->getCode(), + $exception + ); + } + + return $createdAt; + } + + /** + * @return \DateTimeImmutable + */ + public function getCreatedAt(): \DateTimeImmutable + { + return $this->createdAt; + } +} diff --git a/src/Stores/Jobs/Interfaces/JobsStoreInterface.php b/src/Stores/Jobs/Interfaces/JobsStoreInterface.php deleted file mode 100644 index d7ef16b1d6a60b99087a02f5d0e208d2d6e39e05..0000000000000000000000000000000000000000 --- a/src/Stores/Jobs/Interfaces/JobsStoreInterface.php +++ /dev/null @@ -1,7 +0,0 @@ -<?php - -namespace SimpleSAML\Module\accounting\Stores\Jobs\Interfaces; - -interface JobsStoreInterface -{ -} diff --git a/src/Stores/Jobs/Interfaces/JobsStoreMigrationInterface.php b/src/Stores/Jobs/Interfaces/JobsStoreMigrationInterface.php deleted file mode 100644 index 9a935152ec7dcb368c10afbc01c092d46abb17c0..0000000000000000000000000000000000000000 --- a/src/Stores/Jobs/Interfaces/JobsStoreMigrationInterface.php +++ /dev/null @@ -1,12 +0,0 @@ -<?php - -namespace SimpleSAML\Module\accounting\Stores\Jobs\Interfaces; - -interface JobsStoreMigrationInterface -{ - /** - * Run this jobs store migration. - * @return void - */ - public function run(): void; -} diff --git a/tests/src/Entities/AuthenticationEvent/JobTest.php b/tests/src/Entities/AuthenticationEvent/JobTest.php new file mode 100644 index 0000000000000000000000000000000000000000..16824118e8d757a34b08f6e4c8502fa52e12e82f --- /dev/null +++ b/tests/src/Entities/AuthenticationEvent/JobTest.php @@ -0,0 +1,34 @@ +<?php + +namespace SimpleSAML\Test\Module\accounting\Entities\AuthenticationEvent; + +use SimpleSAML\Module\accounting\Entities\AuthenticationEvent; +use SimpleSAML\Module\accounting\Entities\Bases\AbstractPayload; +use SimpleSAML\Module\accounting\Entities\AuthenticationEvent\Job; +use PHPUnit\Framework\TestCase; +use SimpleSAML\Module\accounting\Exceptions\UnexpectedValueException; + +/** + * @covers \SimpleSAML\Module\accounting\Entities\AuthenticationEvent\Job + * @uses \SimpleSAML\Module\accounting\Entities\Bases\AbstractJob + * @uses \SimpleSAML\Module\accounting\Entities\AuthenticationEvent + */ +class JobTest extends TestCase +{ + public function testCanCreateInstanceWithAuthenticationEventEntity(): void + { + $job = new Job(new AuthenticationEvent(['sample' => 'state'])); + + $this->assertInstanceOf(AuthenticationEvent::class, $job->getPayload()); + } + + public function testPayloadMustBeAuthenticationEventOrThrow(): void + { + $payload = new class extends AbstractPayload { + }; + + $this->expectException(UnexpectedValueException::class); + + (new Job($payload)); + } +} diff --git a/tests/src/Entities/AuthenticationEventTest.php b/tests/src/Entities/AuthenticationEventTest.php new file mode 100644 index 0000000000000000000000000000000000000000..bf4ceb2101c946b9abea1fc5bbe8f662d5d88aa3 --- /dev/null +++ b/tests/src/Entities/AuthenticationEventTest.php @@ -0,0 +1,24 @@ +<?php + +namespace SimpleSAML\Test\Module\accounting\Entities; + +use SimpleSAML\Module\accounting\Entities\AuthenticationEvent; +use PHPUnit\Framework\TestCase; + +/** + * @covers \SimpleSAML\Module\accounting\Entities\AuthenticationEvent + */ +class AuthenticationEventTest extends TestCase +{ + protected array $state = [ + 'sample' => 'value', + ]; + + + public function testCanGetState(): void + { + $authenticationEvent = new AuthenticationEvent($this->state); + + $this->assertArrayHasKey('sample', $authenticationEvent->getState()); + } +} diff --git a/tests/src/Entities/Bases/AbstractJobTest.php b/tests/src/Entities/Bases/AbstractJobTest.php new file mode 100644 index 0000000000000000000000000000000000000000..86fa21be640475adb53764e39f71e74f8c8f6cf0 --- /dev/null +++ b/tests/src/Entities/Bases/AbstractJobTest.php @@ -0,0 +1,36 @@ +<?php + +namespace SimpleSAML\Test\Module\accounting\Entities\Bases; + +use SimpleSAML\Module\accounting\Entities\AuthenticationEvent; +use SimpleSAML\Module\accounting\Entities\AuthenticationEvent\Job; +use SimpleSAML\Module\accounting\Entities\Bases\AbstractJob; +use PHPUnit\Framework\TestCase; +use SimpleSAML\Module\accounting\Entities\Bases\AbstractPayload; + +/** + * @covers \SimpleSAML\Module\accounting\Entities\Bases\AbstractJob + * @uses \SimpleSAML\Module\accounting\Entities\AuthenticationEvent\Job + * @uses \SimpleSAML\Module\accounting\Entities\AuthenticationEvent + */ +class AbstractJobTest extends TestCase +{ + protected AbstractPayload $payload; + + protected function setUp(): void + { + $this->payload = new class extends AbstractPayload { + }; + } + + public function testCanSetAndGetPayload(): void + { + $job = new class ($this->payload) extends AbstractJob { + public function run(): void + { + } + }; + + $this->assertInstanceOf(AbstractPayload::class, $job->getPayload()); + } +} diff --git a/tests/src/Services/LoggerServiceTest.php b/tests/src/Services/LoggerServiceTest.php index 364cdbf9b1b7c9dab5104bb354ca1647ed533751..a1ffe30c1dffb0b8199303755c8fa0d5e2c18fe2 100644 --- a/tests/src/Services/LoggerServiceTest.php +++ b/tests/src/Services/LoggerServiceTest.php @@ -24,6 +24,8 @@ class LoggerServiceTest extends TestCase $loggerService->critical('test'); $loggerService->emergency('test'); + $loggerService->emergency('test', ['sample' => 'context']); + $this->assertTrue(true); // Nothing to evaluate } } diff --git a/tests/src/Stores/Connections/Bases/AbstractMigratorTest.php b/tests/src/Stores/Connections/Bases/AbstractMigratorTest.php index 1cd96d8b2a24b11a76e126a4f1dcf69c374f56cc..a9bda1bf9ed49eeb2630b7d6585f2fe7582a397c 100644 --- a/tests/src/Stores/Connections/Bases/AbstractMigratorTest.php +++ b/tests/src/Stores/Connections/Bases/AbstractMigratorTest.php @@ -3,7 +3,7 @@ namespace SimpleSAML\Test\Module\accounting\Stores\Connections\Bases; use Doctrine\DBAL\Schema\AbstractSchemaManager; -use SimpleSAML\Module\accounting\Exceptions\MigrationException; +use SimpleSAML\Module\accounting\Exceptions\StoreException\MigrationException; use SimpleSAML\Module\accounting\ModuleConfiguration; use SimpleSAML\Module\accounting\Services\LoggerService; use SimpleSAML\Module\accounting\Stores\Connections\Bases\AbstractMigrator; @@ -160,7 +160,7 @@ class AbstractMigratorTest extends TestCase public function testCanRunNonImplementedMigrationClasses(): void { -/** @psalm-suppress InvalidArgument Using mock instead of LoggerService instance */ + /** @psalm-suppress InvalidArgument Using mock instead of LoggerService instance */ $migrator = new Migrator($this->connection, $this->loggerServiceMock); $migrator->runSetup(); diff --git a/tests/src/Stores/Connections/DoctrineDbal/Bases/AbstractMigrationTest.php b/tests/src/Stores/Connections/DoctrineDbal/Bases/AbstractMigrationTest.php index bdbef5430da50c68a810fef9a7413fb137d34a8d..954233cdd3778966802b1bb235dc86299a634e82 100644 --- a/tests/src/Stores/Connections/DoctrineDbal/Bases/AbstractMigrationTest.php +++ b/tests/src/Stores/Connections/DoctrineDbal/Bases/AbstractMigrationTest.php @@ -2,6 +2,7 @@ namespace SimpleSAML\Test\Module\accounting\Stores\Connections\DoctrineDbal\Bases; +use SimpleSAML\Module\accounting\Exceptions\StoreException; use SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Bases\AbstractMigration; use PHPUnit\Framework\TestCase; use SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Connection; @@ -14,13 +15,50 @@ use SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\JobsStore\Migrations\V */ class AbstractMigrationTest extends TestCase { - public function testCanInstantiateMigrationClass(): void + protected Connection $connection; + + protected function setUp(): void { - $connection = new Connection(['driver' => 'pdo_sqlite', 'memory' => true,]); + parent::setUp(); + $this->connection = new Connection(['driver' => 'pdo_sqlite', 'memory' => true,]); + } + public function testCanInstantiateMigrationClass(): void + { $this->assertInstanceOf( AbstractMigration::class, - new Version20220601000000CreateJobsTable($connection) + new Version20220601000000CreateJobsTable($this->connection) ); } + + public function testThrowsStoreException(): void + { + $dbalStub = $this->createStub(\Doctrine\DBAL\Connection::class); + $dbalStub->method('createSchemaManager') + ->willThrowException(new \Doctrine\DBAL\Exception('test')); + $connectionStub = $this->createStub(Connection::class); + $connectionStub->method('dbal')->willReturn($dbalStub); + + $this->expectException(StoreException::class); + + (new Version20220601000000CreateJobsTable($connectionStub)); + } + + public function testCanThrowGenericMigrationExceptionOnRun(): void + { + $migration = new class ($this->connection) extends AbstractMigration { + public function run(): void + { + $this->throwGenericMigrationException('test', new \Exception('test')); + } + + public function revert(): void + { + } + }; + + $this->expectException(StoreException\MigrationException::class); + + $migration->run(); + } } diff --git a/tests/src/Stores/Connections/DoctrineDbal/MigratorTest.php b/tests/src/Stores/Connections/DoctrineDbal/MigratorTest.php index f02cd4b554f61a04e27c22b43651fca9c3f3f877..bfa5093c4f3af1461a29695ff47b467e54dcfdf8 100644 --- a/tests/src/Stores/Connections/DoctrineDbal/MigratorTest.php +++ b/tests/src/Stores/Connections/DoctrineDbal/MigratorTest.php @@ -2,9 +2,11 @@ namespace SimpleSAML\Test\Module\accounting\Stores\Connections\DoctrineDbal; +use Doctrine\DBAL\Query\QueryBuilder; use PHPUnit\Framework\TestCase; use Doctrine\DBAL\Schema\AbstractSchemaManager; use SimpleSAML\Module\accounting\Exceptions\InvalidValueException; +use SimpleSAML\Module\accounting\Exceptions\StoreException; use SimpleSAML\Module\accounting\ModuleConfiguration; use SimpleSAML\Module\accounting\Services\LoggerService; use SimpleSAML\Module\accounting\Stores\Connections\Bases\AbstractMigrator; @@ -75,7 +77,7 @@ class MigratorTest extends TestCase $this->assertTrue($this->schemaManager->tablesExist([$this->tableName])); } - public function testRunningMigratiorSetupMultipleTimesLogsWarning(): void + public function testRunningMigratorSetupMultipleTimesLogsWarning(): void { $this->loggerServiceMock ->expects($this->once()) @@ -144,6 +146,90 @@ class MigratorTest extends TestCase $this->assertNotEmpty($migrator->getImplementedMigrationClasses()); } + public function testThrowsStoreExceptionOnInitialization(): void + { + $dbalStub = $this->createStub(\Doctrine\DBAL\Connection::class); + $dbalStub->method('createSchemaManager')->willThrowException(new \Doctrine\DBAL\Exception('test')); + $connectionStub = $this->createStub(Connection::class); + $connectionStub->method('dbal')->willReturn($dbalStub); + + $this->expectException(StoreException::class); + + /** @psalm-suppress InvalidArgument */ + (new Migrator($connectionStub, $this->loggerServiceMock)); + } + + public function testThrowsStoreExceptionOnNeedsSetup(): void + { + $schemaManagerStub = $this->createStub(AbstractSchemaManager::class); + $schemaManagerStub->method('tablesExist') + ->willThrowException(new \Doctrine\DBAL\Exception('test')); + $dbalStub = $this->createStub(\Doctrine\DBAL\Connection::class); + $dbalStub->method('createSchemaManager')->willReturn($schemaManagerStub); + $connectionStub = $this->createStub(Connection::class); + $connectionStub->method('dbal')->willReturn($dbalStub); + + /** @psalm-suppress InvalidArgument */ + $migrator = new Migrator($connectionStub, $this->loggerServiceMock); + + $this->expectException(StoreException::class); + + $migrator->needsSetup(); + } + + public function testThrowsStoreExceptionOnCreateMigrationsTable(): void + { + $schemaManagerStub = $this->createStub(AbstractSchemaManager::class); + $schemaManagerStub->method('tablesExist') + ->willReturn(false); + $dbalStub = $this->createStub(\Doctrine\DBAL\Connection::class); + $dbalStub->method('createSchemaManager')->willReturn($schemaManagerStub); + $connectionStub = $this->createStub(Connection::class); + $connectionStub->method('dbal')->willReturn($dbalStub); + + /** @psalm-suppress InvalidArgument */ + $migrator = new Migrator($connectionStub, $this->loggerServiceMock); + + $this->expectException(StoreException::class); + + $migrator->runSetup(); + } + + public function testThrowsStoreExceptionOnMarkingImplementedClass(): void + { + $queryBuilderStub = $this->createStub(QueryBuilder::class); + $queryBuilderStub->method('insert') + ->willThrowException(new \Doctrine\DBAL\Exception('test')); + $dbalStub = $this->createStub(\Doctrine\DBAL\Connection::class); + $dbalStub->method('createQueryBuilder')->willReturn($queryBuilderStub); + $connectionStub = $this->createStub(Connection::class); + $connectionStub->method('dbal')->willReturn($dbalStub); + $connectionStub->method('preparePrefixedTableName')->willReturn(Migrator::TABLE_NAME); + + /** @psalm-suppress InvalidArgument */ + $migrator = new Migrator($connectionStub, $this->loggerServiceMock); + $migrator->runSetup(); + + $this->expectException(StoreException::class); + + $migrator->runMigrationClasses([JobsStore\Migrations\Version20220601000000CreateJobsTable::class]); + } + + public function testThrowsStoreExceptionOnGetImplementedMigrationClasses(): void + { + $schemaManagerStub = $this->createStub(AbstractSchemaManager::class); + $dbalStub = $this->createStub(\Doctrine\DBAL\Connection::class); + $dbalStub->method('createQueryBuilder')->willThrowException(new \Doctrine\DBAL\Exception('test')); + $dbalStub->method('createSchemaManager')->willReturn($schemaManagerStub); + $connectionStub = $this->createStub(Connection::class); + $connectionStub->method('dbal')->willReturn($dbalStub); + + $this->expectException(StoreException::class); + + /** @psalm-suppress InvalidArgument */ + (new Migrator($connectionStub, $this->loggerServiceMock))->getImplementedMigrationClasses(); + } + protected function getSampleMigrationsDirectory(): string { return $this->moduleConfiguration->getModuleSourceDirectory() . DIRECTORY_SEPARATOR . diff --git a/tests/src/Stores/Jobs/DoctrineDbal/JobsStore/Migrations/Version20220601000000CreateJobsTableTest.php b/tests/src/Stores/Jobs/DoctrineDbal/JobsStore/Migrations/Version20220601000000CreateJobsTableTest.php index ad2b7e99a145e3d088e2c3c5502f0e468bb7e618..011b249112ee52f246d272fed33acc107639241d 100644 --- a/tests/src/Stores/Jobs/DoctrineDbal/JobsStore/Migrations/Version20220601000000CreateJobsTableTest.php +++ b/tests/src/Stores/Jobs/DoctrineDbal/JobsStore/Migrations/Version20220601000000CreateJobsTableTest.php @@ -2,6 +2,8 @@ namespace SimpleSAML\Test\Module\accounting\Stores\Jobs\DoctrineDbal\JobsStore\Migrations; +use Doctrine\DBAL\Schema\AbstractSchemaManager; +use SimpleSAML\Module\accounting\Exceptions\StoreException\MigrationException; use SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Connection; use SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\JobsStore; use SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\JobsStore\Migrations\Version20220601000000CreateJobsTable; @@ -34,4 +36,36 @@ class Version20220601000000CreateJobsTableTest extends TestCase $migration->revert(); $this->assertFalse($this->schemaManager->tablesExist($this->tableName)); } + + public function testRunThrowsMigrationException(): void + { + $connectionStub = $this->createStub(Connection::class); + $dbalStub = $this->createStub(\Doctrine\DBAL\Connection::class); + $schemaManagerStub = $this->createStub(AbstractSchemaManager::class); + + $connectionStub->method('dbal')->willReturn($dbalStub); + $dbalStub->method('createSchemaManager')->willReturn($schemaManagerStub); + $schemaManagerStub->method('createTable') + ->willThrowException(new \Doctrine\DBAL\Exception('test')); + + $migration = new Version20220601000000CreateJobsTable($connectionStub); + $this->expectException(MigrationException::class); + $migration->run(); + } + + public function testRevertThrowsMigrationException(): void + { + $connectionStub = $this->createStub(Connection::class); + $dbalStub = $this->createStub(\Doctrine\DBAL\Connection::class); + $schemaManagerStub = $this->createStub(AbstractSchemaManager::class); + + $connectionStub->method('dbal')->willReturn($dbalStub); + $dbalStub->method('createSchemaManager')->willReturn($schemaManagerStub); + $schemaManagerStub->method('dropTable') + ->willThrowException(new \Doctrine\DBAL\Exception('test')); + + $migration = new Version20220601000000CreateJobsTable($connectionStub); + $this->expectException(MigrationException::class); + $migration->revert(); + } } diff --git a/tests/src/Stores/Jobs/DoctrineDbal/JobsStore/Migrations/Version20220601000100CreateFailedJobsTableTest.php b/tests/src/Stores/Jobs/DoctrineDbal/JobsStore/Migrations/Version20220601000100CreateFailedJobsTableTest.php index c99092eef1c563ff1ac8d685ac52256866616cea..d6624b3f291282e481153e8cd1f7d413d25802ff 100644 --- a/tests/src/Stores/Jobs/DoctrineDbal/JobsStore/Migrations/Version20220601000100CreateFailedJobsTableTest.php +++ b/tests/src/Stores/Jobs/DoctrineDbal/JobsStore/Migrations/Version20220601000100CreateFailedJobsTableTest.php @@ -2,6 +2,8 @@ namespace SimpleSAML\Test\Module\accounting\Stores\Jobs\DoctrineDbal\JobsStore\Migrations; +use Doctrine\DBAL\Schema\AbstractSchemaManager; +use SimpleSAML\Module\accounting\Exceptions\StoreException\MigrationException; use SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Connection; use SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\JobsStore; use SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\JobsStore\Migrations; @@ -35,4 +37,36 @@ class Version20220601000100CreateFailedJobsTableTest extends TestCase $migration->revert(); $this->assertFalse($this->schemaManager->tablesExist($this->tableName)); } + + public function testRunThrowsMigrationException(): void + { + $connectionStub = $this->createStub(Connection::class); + $dbalStub = $this->createStub(\Doctrine\DBAL\Connection::class); + $schemaManagerStub = $this->createStub(AbstractSchemaManager::class); + + $connectionStub->method('dbal')->willReturn($dbalStub); + $dbalStub->method('createSchemaManager')->willReturn($schemaManagerStub); + $schemaManagerStub->method('createTable') + ->willThrowException(new \Doctrine\DBAL\Exception('test')); + + $migration = new Migrations\Version20220601000100CreateFailedJobsTable($connectionStub); + $this->expectException(MigrationException::class); + $migration->run(); + } + + public function testRevertThrowsMigrationException(): void + { + $connectionStub = $this->createStub(Connection::class); + $dbalStub = $this->createStub(\Doctrine\DBAL\Connection::class); + $schemaManagerStub = $this->createStub(AbstractSchemaManager::class); + + $connectionStub->method('dbal')->willReturn($dbalStub); + $dbalStub->method('createSchemaManager')->willReturn($schemaManagerStub); + $schemaManagerStub->method('dropTable') + ->willThrowException(new \Doctrine\DBAL\Exception('test')); + + $migration = new Migrations\Version20220601000100CreateFailedJobsTable($connectionStub); + $this->expectException(MigrationException::class); + $migration->revert(); + } } diff --git a/tests/src/Stores/Jobs/DoctrineDbal/JobsStoreTest.php b/tests/src/Stores/Jobs/DoctrineDbal/JobsStoreTest.php index cbb4013c7b590a7a8120deae3c2c9185c27191d7..3686a6f4acf623a132a381dcec22247e86dc9caa 100644 --- a/tests/src/Stores/Jobs/DoctrineDbal/JobsStoreTest.php +++ b/tests/src/Stores/Jobs/DoctrineDbal/JobsStoreTest.php @@ -2,11 +2,16 @@ namespace SimpleSAML\Test\Module\accounting\Stores\Jobs\DoctrineDbal; -use PHPUnit\Framework\MockObject\MockObject; +use SimpleSAML\Module\accounting\Entities\AuthenticationEvent; +use SimpleSAML\Module\accounting\Entities\Bases\AbstractJob; +use SimpleSAML\Module\accounting\Entities\Bases\AbstractPayload; +use SimpleSAML\Module\accounting\Entities\Interfaces\JobInterface; +use SimpleSAML\Module\accounting\Exceptions\StoreException; use SimpleSAML\Module\accounting\ModuleConfiguration; use SimpleSAML\Module\accounting\Services\LoggerService; use SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Connection; use SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Factory; +use SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Migrator; use SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\JobsStore; use PHPUnit\Framework\TestCase; @@ -15,26 +20,73 @@ use PHPUnit\Framework\TestCase; * @uses \SimpleSAML\Module\accounting\ModuleConfiguration * @uses \SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Connection * @uses \SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Factory + * @uses \SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Migrator + * @uses \SimpleSAML\Module\accounting\Stores\Connections\Bases\AbstractMigrator + * @uses \SimpleSAML\Module\accounting\Helpers\FilesystemHelper + * @uses \SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Bases\AbstractMigration + * @uses \SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\JobsStore\Migrations\Version20220601000000CreateJobsTable + * @uses \SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\JobsStore\Migrations\Version20220601000100CreateFailedJobsTable + * @uses \SimpleSAML\Module\accounting\Entities\Bases\AbstractJob + * @uses \SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\RawJob + * @uses \SimpleSAML\Module\accounting\Entities\AuthenticationEvent + * @uses \SimpleSAML\Module\accounting\Entities\AuthenticationEvent\Job */ class JobsStoreTest extends TestCase { protected ModuleConfiguration $moduleConfiguration; - protected \PHPUnit\Framework\MockObject\Stub $factory; + protected \PHPUnit\Framework\MockObject\Stub $factoryStub; protected Connection $connection; + protected \PHPUnit\Framework\MockObject\Stub $loggerServiceStub; + protected Migrator $migrator; protected function setUp(): void { // Configuration directory is set by phpunit using php ENV setting feature (check phpunit.xml). $this->moduleConfiguration = new ModuleConfiguration('module_accounting.php'); $this->connection = new Connection(['driver' => 'pdo_sqlite', 'memory' => true,]); - $this->factory = $this->createStub(Factory::class); - $this->factory->method('buildConnection')->willReturn($this->connection); + + $this->loggerServiceStub = $this->createStub(LoggerService::class); + + /** @psalm-suppress InvalidArgument */ + $this->migrator = new Migrator($this->connection, $this->loggerServiceStub); + + $this->factoryStub = $this->createStub(Factory::class); + $this->factoryStub->method('buildConnection')->willReturn($this->connection); + $this->factoryStub->method('buildMigrator')->willReturn($this->migrator); + } + + public function testSetupDependsOnMigratorSetup(): void + { + /** @psalm-suppress InvalidArgument */ + $jobsStore = new JobsStore($this->moduleConfiguration, $this->factoryStub, $this->loggerServiceStub); + + $this->assertTrue($this->migrator->needsSetup()); + $this->assertTrue($jobsStore->needsSetup()); + + $jobsStore->runSetup(); + + $this->assertFalse($jobsStore->needsSetup()); + $this->assertFalse($this->migrator->needsSetup()); + } + + public function testSetupDependsOnMigrations(): void + { + /** @psalm-suppress InvalidArgument */ + $jobsStore = new JobsStore($this->moduleConfiguration, $this->factoryStub, $this->loggerServiceStub); + + // Run migrator setup beforehand, so it only depends on JobsStore migrations setup + $this->migrator->runSetup(); + $this->assertTrue($jobsStore->needsSetup()); + + $jobsStore->runSetup(); + + $this->assertFalse($jobsStore->needsSetup()); } public function testCanGetPrefixedTableNames(): void { /** @psalm-suppress InvalidArgument */ - $jobsStore = new JobsStore($this->moduleConfiguration, $this->factory); + $jobsStore = new JobsStore($this->moduleConfiguration, $this->factoryStub, $this->loggerServiceStub); $tableNameJobs = $this->connection->preparePrefixedTableName(JobsStore::TABLE_NAME_JOBS); $tableNameFailedJobs = $this->connection->preparePrefixedTableName(JobsStore::TABLE_NAME_FAILED_JOBS); @@ -42,4 +94,181 @@ class JobsStoreTest extends TestCase $this->assertSame($tableNameJobs, $jobsStore->getPrefixedTableNameJobs()); $this->assertSame($tableNameFailedJobs, $jobsStore->getPrefixedTableNameFailedJobs()); } + + public function testCanEnqueueJob(): void + { + /** @psalm-suppress InvalidArgument */ + $jobsStore = new JobsStore($this->moduleConfiguration, $this->factoryStub, $this->loggerServiceStub); + $jobsStore->runSetup(); + + $payloadStub = $this->createStub(AbstractPayload::class); + $jobStub = $this->createStub(AbstractJob::class); + $jobStub->method('getPayload')->willReturn($payloadStub); + + $queryBuilder = $this->connection->dbal()->createQueryBuilder(); + $queryBuilder->select('COUNT(id) as jobsCount')->from($jobsStore->getPrefixedTableNameJobs())->fetchOne(); + + $this->assertSame(0, (int) $queryBuilder->executeQuery()->fetchOne()); + + $jobsStore->enqueue($jobStub); + + $this->assertSame(1, (int) $queryBuilder->executeQuery()->fetchOne()); + + $jobsStore->enqueue($jobStub); + $jobsStore->enqueue($jobStub); + + $this->assertSame(3, (int) $queryBuilder->executeQuery()->fetchOne()); + } + + public function testEnqueueThrowsStoreExceptionOnNonSetupRun(): void + { + /** @psalm-suppress InvalidArgument */ + $jobsStore = new JobsStore($this->moduleConfiguration, $this->factoryStub, $this->loggerServiceStub); + // Don't run setup, so we get exception + //$jobsStore->runSetup(); + + $payloadStub = $this->createStub(AbstractPayload::class); + $jobStub = $this->createStub(AbstractJob::class); + $jobStub->method('getPayload')->willReturn($payloadStub); + + $this->expectException(StoreException::class); + + $jobsStore->enqueue($jobStub); + } + + public function testEnqueueThrowsStoreExceptionOnInvalidType(): void + { + /** @psalm-suppress InvalidArgument */ + $jobsStore = new JobsStore($this->moduleConfiguration, $this->factoryStub, $this->loggerServiceStub); + // Don't run setup, so we get exception + $jobsStore->runSetup(); + + $payloadStub = $this->createStub(AbstractPayload::class); + $jobStub = $this->createStub(AbstractJob::class); + $jobStub->method('getPayload')->willReturn($payloadStub); + $invalidType = str_pad('invalid-type', JobsStore::COLUMN_LENGTH_TYPE + 1); + + $this->expectException(StoreException::class); + + $jobsStore->enqueue($jobStub, $invalidType); + } + + public function testCanGetNextJob(): void + { + /** @psalm-suppress InvalidArgument */ + $jobsStore = new JobsStore($this->moduleConfiguration, $this->factoryStub, $this->loggerServiceStub); + $jobsStore->runSetup(); + + $payloadStub = $this->createStub(AbstractPayload::class); + $jobStub = $this->createStub(AbstractJob::class); + $jobStub->method('getPayload')->willReturn($payloadStub); + + $queryBuilder = $this->connection->dbal()->createQueryBuilder(); + $queryBuilder->select('COUNT(id) as jobsCount')->from($jobsStore->getPrefixedTableNameJobs())->fetchOne(); + + $this->assertSame(0, (int) $queryBuilder->executeQuery()->fetchOne()); + + $jobsStore->enqueue($jobStub); + + $this->assertNotEmpty($jobsStore->getNext()->fetchOne()); + $this->assertNotEmpty($jobsStore->getNext(get_class($jobStub))->fetchOne()); + + + $jobsStore->enqueue($jobStub, 'sample-type'); + $this->assertNotEmpty($jobsStore->getNext('sample-type')->fetchOne()); + } + + public function testGetNextThrowsOnNonSetupRun(): void + { + /** @psalm-suppress InvalidArgument */ + $jobsStore = new JobsStore($this->moduleConfiguration, $this->factoryStub, $this->loggerServiceStub); + // Don't run setup, so we get exception + //$jobsStore->runSetup(); + + $payloadStub = $this->createStub(AbstractPayload::class); + $jobStub = $this->createStub(AbstractJob::class); + $jobStub->method('getPayload')->willReturn($payloadStub); + + $this->expectException(StoreException::class); + + $jobsStore->getNext(); + } + + public function testCanDequeueJob(): void + { + /** @psalm-suppress InvalidArgument */ + $jobsStore = new JobsStore($this->moduleConfiguration, $this->factoryStub, $this->loggerServiceStub); + $jobsStore->runSetup(); + + $payloadStub = $this->createStub(AbstractPayload::class); + $jobStub = $this->createStub(AbstractJob::class); + $jobStub->method('getPayload')->willReturn($payloadStub); + + $queryBuilder = $this->connection->dbal()->createQueryBuilder(); + $queryBuilder->select('COUNT(id) as jobsCount')->from($jobsStore->getPrefixedTableNameJobs())->fetchOne(); + + $this->assertSame(0, (int) $queryBuilder->executeQuery()->fetchOne()); + + $jobsStore->enqueue($jobStub); + $jobsStore->enqueue($jobStub); + + $this->assertSame(2, (int) $queryBuilder->executeQuery()->fetchOne()); + + $jobsStore->dequeue(); + + $this->assertSame(1, (int) $queryBuilder->executeQuery()->fetchOne()); + } + + public function testCanDequeueSpecificJobType(): void + { + /** @psalm-suppress InvalidArgument */ + $jobsStore = new JobsStore($this->moduleConfiguration, $this->factoryStub, $this->loggerServiceStub); + $jobsStore->runSetup(); + + $payloadStub = $this->createStub(AbstractPayload::class); + $jobStub = $this->createStub(AbstractJob::class); + $jobStub->method('getPayload')->willReturn($payloadStub); + + $authenticationEvent = new AuthenticationEvent(['sample-state']); + $authenticationEventJob = new AuthenticationEvent\Job($authenticationEvent); + + $queryBuilder = $this->connection->dbal()->createQueryBuilder(); + $queryBuilder->select('COUNT(id) as jobsCount')->from($jobsStore->getPrefixedTableNameJobs())->fetchOne(); + + $this->assertSame(0, (int) $queryBuilder->executeQuery()->fetchOne()); + + $jobsStore->enqueue($jobStub, 'test-type'); + $jobsStore->enqueue($authenticationEventJob); + + $this->assertSame(2, (int) $queryBuilder->executeQuery()->fetchOne()); + + $this->assertInstanceOf(AuthenticationEvent\Job::class, $jobsStore->dequeue(AuthenticationEvent\Job::class)); + + $this->assertSame(1, (int) $queryBuilder->executeQuery()->fetchOne()); + + $this->assertNull($jobsStore->dequeue(AuthenticationEvent::class)); + + $this->assertSame(1, (int) $queryBuilder->executeQuery()->fetchOne()); + } + + public function testDequeueThrowsWhenSetupNotRun(): void + { + /** @psalm-suppress InvalidArgument */ + $jobsStore = new JobsStore($this->moduleConfiguration, $this->factoryStub, $this->loggerServiceStub); +// $jobsStore->runSetup(); + + $payloadStub = $this->createStub(AbstractPayload::class); + $jobStub = $this->createStub(AbstractJob::class); + $jobStub->method('getPayload')->willReturn($payloadStub); + + $this->expectException(StoreException::class); + + $jobsStore->dequeue('test-type'); + } + + public function testCanContinueSearchingInCaseOfJobDeletion(): void + { + // TODO try to simulate job deletion + $this->markTestIncomplete(); + } } diff --git a/tests/src/Stores/Jobs/DoctrineDbal/RawJobTest.php b/tests/src/Stores/Jobs/DoctrineDbal/RawJobTest.php new file mode 100644 index 0000000000000000000000000000000000000000..6d9fe229e710b367db088a8e3aa071d77b56ee9b --- /dev/null +++ b/tests/src/Stores/Jobs/DoctrineDbal/RawJobTest.php @@ -0,0 +1,121 @@ +<?php + +namespace SimpleSAML\Test\Module\accounting\Stores\Jobs\DoctrineDbal; + +use Doctrine\DBAL\Platforms\AbstractPlatform; +use Doctrine\DBAL\Platforms\SqlitePlatform; +use SimpleSAML\Module\accounting\Entities\AuthenticationEvent; +use SimpleSAML\Module\accounting\Exceptions\UnexpectedValueException; +use SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\JobsStore; +use SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\RawJob; +use PHPUnit\Framework\TestCase; + +/** + * @covers \SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\RawJob + * @uses \SimpleSAML\Module\accounting\Entities\AuthenticationEvent + */ +class RawJobTest extends TestCase +{ + protected AuthenticationEvent $authenticationEvent; + protected array $validRawRow; + protected \PHPUnit\Framework\MockObject\Stub $abstractPlatformStub; + + protected function setUp(): void + { + $this->abstractPlatformStub = $this->createStub(AbstractPlatform::class); + $this->authenticationEvent = new AuthenticationEvent(['sample' => 'state']); + $this->validRawRow = [ + JobsStore::COLUMN_NAME_ID => 1, + JobsStore::COLUMN_NAME_PAYLOAD => serialize($this->authenticationEvent), + JobsStore::COLUMN_NAME_TYPE => get_class($this->authenticationEvent), + JobsStore::COLUMN_NAME_CREATED_AT => '2022-08-17 13:26:12', + ]; + } + + public function testCanInstantiateValidRawJob(): void + { + $abstractPlatform = new SqlitePlatform(); + $rawJob = new RawJob($this->validRawRow, $abstractPlatform); + $this->assertSame($rawJob->getId(), $this->validRawRow[JobsStore::COLUMN_NAME_ID]); + $this->assertEquals($rawJob->getPayload(), $this->authenticationEvent); + $this->assertSame($rawJob->getType(), $this->validRawRow[JobsStore::COLUMN_NAME_TYPE]); + $this->assertInstanceOf(\DateTimeImmutable::class, $rawJob->getCreatedAt()); + } + + public function testThrowsOnEmptyColumn(): void + { + $invalidRawRow = $this->validRawRow; + unset($invalidRawRow[JobsStore::COLUMN_NAME_ID]); + + $this->expectException(UnexpectedValueException::class); + + /** @psalm-suppress InvalidArgument */ + new RawJob($invalidRawRow, $this->abstractPlatformStub); + } + + public function testThrowsOnNonNumericId(): void + { + $invalidRawRow = $this->validRawRow; + $invalidRawRow[JobsStore::COLUMN_NAME_ID] = 'a'; + + $this->expectException(UnexpectedValueException::class); + + /** @psalm-suppress InvalidArgument */ + new RawJob($invalidRawRow, $this->abstractPlatformStub); + } + + public function testThrowsOnNonStringPayload(): void + { + $invalidRawRow = $this->validRawRow; + $invalidRawRow[JobsStore::COLUMN_NAME_PAYLOAD] = 123; + + $this->expectException(UnexpectedValueException::class); + + /** @psalm-suppress InvalidArgument */ + new RawJob($invalidRawRow, $this->abstractPlatformStub); + } + + public function testThrowsOnNonAbstractPayload(): void + { + $invalidRawRow = $this->validRawRow; + $invalidRawRow[JobsStore::COLUMN_NAME_PAYLOAD] = serialize('abc'); + + $this->expectException(UnexpectedValueException::class); + + /** @psalm-suppress InvalidArgument */ + new RawJob($invalidRawRow, $this->abstractPlatformStub); + } + + public function testThrowsOnNonStringType(): void + { + $invalidRawRow = $this->validRawRow; + $invalidRawRow[JobsStore::COLUMN_NAME_TYPE] = 123; + + $this->expectException(UnexpectedValueException::class); + + /** @psalm-suppress InvalidArgument */ + new RawJob($invalidRawRow, $this->abstractPlatformStub); + } + + public function testThrowsOnNonStringCreatedAt(): void + { + $invalidRawRow = $this->validRawRow; + $invalidRawRow[JobsStore::COLUMN_NAME_CREATED_AT] = 123; + + $this->expectException(UnexpectedValueException::class); + + /** @psalm-suppress InvalidArgument */ + new RawJob($invalidRawRow, $this->abstractPlatformStub); + } + + public function testThrowsOnNonValidCreatedAt(): void + { + $invalidRawRow = $this->validRawRow; + $invalidRawRow[JobsStore::COLUMN_NAME_CREATED_AT] = '123'; + + $this->expectException(UnexpectedValueException::class); + + /** @psalm-suppress InvalidArgument */ + new RawJob($invalidRawRow, $this->abstractPlatformStub); + } +}