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