From 95e802430d4458b2a99316e747ae0ad81cd321ba Mon Sep 17 00:00:00 2001 From: Vladyslav Vildanov <117659936+vladvildanov@users.noreply.github.com> Date: Tue, 20 Jan 2026 17:45:10 +0200 Subject: [PATCH] Added testing with SSL connection (#1624) * Added testing with SSL connection * Update CHANGELOG.md * Revert changes * Codestyle fixes * Add version restriction * Fixed issue with connecting to SSL port to check redis version * Mark tests as relay-incompatible * Codestyle fixes --- .github/docker-compose.yml | 1 + CHANGELOG.md | 3 + phpunit.xml.dist | 9 +++ tests/PHPUnit/PredisTestCase.php | 118 +++++++++++++++++++++++++++-- tests/Predis/SSLTest.php | 126 +++++++++++++++++++++++++++++++ 5 files changed, 251 insertions(+), 6 deletions(-) create mode 100644 tests/Predis/SSLTest.php diff --git a/.github/docker-compose.yml b/.github/docker-compose.yml index 1fe73341..d4090c77 100644 --- a/.github/docker-compose.yml +++ b/.github/docker-compose.yml @@ -71,6 +71,7 @@ services: command: ${REDIS_EXTRA_ARGS:---enable-debug-command yes --enable-module-command yes --tls-auth-clients optional --save "" --requirepass "foobar"} ports: - "6372-6377:6372-6377" + - "27379-27384:27379-27384" volumes: - "./dockers/cluster:/redis/work" profiles: diff --git a/CHANGELOG.md b/CHANGELOG.md index 067c28fa..86805145 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,9 @@ - Added retry support (#1616) - Added support for VRANGE command (#1623) +### Maintenance +- Added testing with SSL connection (#1624) + ## v3.3.0 (2025-11-24) ### Added - Added cluster support for `XADD`, `XDEL` and `XRANGE` (#1587) diff --git a/phpunit.xml.dist b/phpunit.xml.dist index abeeeaaa..51382a29 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -55,6 +55,15 @@ + + + + + + isUnprotectedTest()) { $port = constant('REDIS_UNPROTECTED_SERVER_PORT'); $password = ''; + } elseif ($this->isSSLTest()) { + $port = getenv('REDIS_SSL_PORT'); } else { $port = constant('REDIS_SERVER_PORT'); } return [ - 'scheme' => 'tcp', + 'scheme' => $this->isSSLTest() ? 'tls' : 'tcp', 'host' => constant('REDIS_SERVER_HOST'), 'port' => $port, 'database' => constant('REDIS_SERVER_DBNUM'), @@ -282,6 +284,28 @@ abstract class PredisTestCase extends PHPUnit\Framework\TestCase ], $options ); + + if ($this->isSSLTest()) { + $options = array_merge($options, [ + 'parameters' => [ + 'ssl' => [ + 'cafile' => getenv('CLUSTER_CA_CERT_PATH'), + 'verify_peer' => true, + 'verify_peer_name' => false, + ], + ], + ]); + } + } else { + if ($this->isSSLTest()) { + $parameters = array_merge($parameters, [ + 'ssl' => [ + 'cafile' => getenv('STANDALONE_CA_CERT_PATH'), + 'verify_peer' => true, + 'verify_peer_name' => false, + ], + ]); + } } $client = new Client($parameters, $options); @@ -294,6 +318,54 @@ abstract class PredisTestCase extends PHPUnit\Framework\TestCase return $client; } + /** + * Creates a client for version checking without SSL configuration. + * This is used to check Redis version before attempting SSL connection. + * + * @return Client + */ + protected function createClientForVersionCheck(): Client + { + // For SSL tests, temporarily override to use non-SSL configuration + $isSSL = $this->isSSLTest(); + $isCluster = $this->isClusterTest(); + + if ($isSSL && $isCluster) { + // For cluster SSL tests, use non-SSL cluster endpoints + $endpoints = explode(',', constant('REDIS_CLUSTER_ENDPOINTS')); + $parameters = array_map(static function (string $elem) { + return 'tcp://' . $elem; + }, $endpoints); + } elseif ($isSSL) { + // For standalone SSL tests, use non-SSL port + $parameters = [ + 'scheme' => 'tcp', + 'host' => constant('REDIS_SERVER_HOST'), + 'port' => constant('REDIS_SERVER_PORT'), + 'database' => constant('REDIS_SERVER_DBNUM'), + 'password' => getenv('REDIS_PASSWORD') ?: constant('REDIS_PASSWORD'), + ]; + } else { + // For non-SSL tests, use default parameters + $parameters = $this->getDefaultParametersArray(); + } + + $commandsFactory = $this->getCommandFactory(); + $options = array_merge( + ['commands' => $commandsFactory], + getenv('USE_RELAY') ? ['connections' => 'relay'] : [] + ); + + if ($isCluster) { + $options['cluster'] = 'redis'; + } + + $client = new Client($parameters, $options); + $client->connect(); + + return $client; + } + /** * Returns a basic mock object of a connection to a single Redis node. * @@ -371,7 +443,9 @@ abstract class PredisTestCase extends PHPUnit\Framework\TestCase if (isset($this->info)) { $info = $this->info; } else { - $client = $this->createClient(null, null, true); + // For SSL tests, connect to non-SSL port to check version first + // This prevents connection failures on Redis < 7.2.0 which doesn't support SSL + $client = $this->createClientForVersionCheck(); $info = array_change_key_case($client->info()); $this->info = $info; } @@ -383,7 +457,7 @@ abstract class PredisTestCase extends PHPUnit\Framework\TestCase // Redis < 2.6 $version = $info['redis_version']; } else { - $client = $this->createClient(null, null, true); + $client = $this->createClientForVersionCheck(); $connection = $client->getConnection(); throw new RuntimeException("Unable to retrieve a valid server info payload from $connection"); } @@ -607,6 +681,34 @@ abstract class PredisTestCase extends PHPUnit\Framework\TestCase && in_array('cluster', $annotations['method']['group'], true); } + /** + * Check annotations if it's matches to SSL test scenario. + * + * @return bool + */ + protected function isSSLTest(): bool + { + $annotations = TestUtil::parseTestMethodAnnotations( + get_class($this), + $this->getName(false) + ); + + $annotationExists = isset($annotations['method']['requiresRedisVersion']); + + if (!$annotationExists) { + foreach ($this->modulesMapping as $module => $configuration) { + if (isset($annotations['method'][$configuration['annotation']])) { + $annotationExists = true; + } + } + } + + return $annotationExists + && isset($annotations['method']['group']) + && in_array('connected', $annotations['method']['group'], true) + && in_array('ssl', $annotations['method']['group'], true); + } + /** * Check annotations if it's matches to stack test scenario. * @@ -646,10 +748,14 @@ abstract class PredisTestCase extends PHPUnit\Framework\TestCase */ protected function prepareClusterEndpoints(): array { - $endpoints = explode(',', constant('REDIS_CLUSTER_ENDPOINTS')); + $endpoints = explode( + ',', + constant($this->isSSLTest() ? 'SSL_REDIS_CLUSTER_ENDPOINTS' : 'REDIS_CLUSTER_ENDPOINTS') + ); + $scheme = $this->isSSLTest() ? 'tls' : 'tcp'; - return array_map(static function (string $elem) { - return 'tcp://' . $elem; + return array_map(static function (string $elem) use ($scheme) { + return "{$scheme}://" . $elem; }, $endpoints); } } diff --git a/tests/Predis/SSLTest.php b/tests/Predis/SSLTest.php new file mode 100644 index 00000000..3533eb79 --- /dev/null +++ b/tests/Predis/SSLTest.php @@ -0,0 +1,126 @@ += 7.2.0 + * @return void + */ + public function testExecuteCommandOverSSLConnection() + { + $redis = $this->createClient(); + $this->assertEquals('PONG', $redis->ping()); + } + + /** + * @group connected + * @group ssl + * @group relay-incompatible + * @requiresRedisVersion >= 7.2.0 + * @return void + */ + public function testExecuteCommandOverSSLConnectionFailsOnIncorrectCertificate() + { + $redis = new Client($this->getDefaultParametersArray() + [ + 'ssl' => ['cafile' => '/tmp/invalid.crt', 'verify_peer' => true, 'verify_peer_name' => false]] + ); + + $this->expectException(StreamInitException::class); + $this->expectExceptionMessage('Error while switching to encrypted communication'); + + $redis->ping(); + } + + /** + * @group connected + * @group ssl + * @group relay-incompatible + * @requiresRedisVersion >= 7.2.0 + * @return void + */ + public function testExecuteCommandOverSSLConnectionWithoutSSLConfig() + { + $redis = new Client($this->getDefaultParametersArray()); + + $this->expectException(StreamInitException::class); + $this->expectExceptionMessage('Error while switching to encrypted communication'); + + $redis->ping(); + } + + /** + * @group connected + * @group ssl + * @group cluster + * @group relay-incompatible + * @requiresRedisVersion >= 7.2.0 + * @return void + */ + public function testClusterExecuteCommandOverSSLConnection() + { + $redis = $this->createClient(); + $redis->set('foo', 'bar'); + $this->assertEquals('bar', $redis->get('foo')); + } + + /** + * @group connected + * @group ssl + * @group cluster + * @group relay-incompatible + * @requiresRedisVersion >= 7.2.0 + * @return void + */ + public function testClusterExecuteCommandOverSSLConnectionFailsOnIncorrectCertificate() + { + $redis = new Client($this->getDefaultParametersArray(), [ + 'cluster' => 'redis', + 'parameters' => [ + 'ssl' => ['cafile' => '/tmp/invalid.crt', 'verify_peer' => true, 'verify_peer_name' => false], + ], + ]); + + $this->expectException(StreamInitException::class); + $this->expectExceptionMessage('Error while switching to encrypted communication'); + + $redis->set('foo', 'bar'); + } + + /** + * @group connected + * @group ssl + * @group cluster + * @group relay-incompatible + * @requiresRedisVersion >= 7.2.0 + * @return void + */ + public function testClusterExecuteCommandOverSSLConnectionWithoutSSLConfig() + { + $redis = new Client($this->getDefaultParametersArray(), [ + 'cluster' => 'redis', + ]); + + $this->expectException(StreamInitException::class); + $this->expectExceptionMessage('Error while switching to encrypted communication'); + + $redis->set('foo', 'bar'); + } +}