From c2668333e2fb4c92a029671123b956b559f2c678 Mon Sep 17 00:00:00 2001 From: Ralf Lang Date: Wed, 8 Apr 2026 07:03:25 +0200 Subject: [PATCH 1/4] style: php-cs-fixer --- lib/Horde/SessionHandler.php | 29 ++--- lib/Horde/SessionHandler/Exception.php | 7 +- lib/Horde/SessionHandler/Storage.php | 11 +- lib/Horde/SessionHandler/Storage/Builtin.php | 27 ++-- lib/Horde/SessionHandler/Storage/External.php | 7 +- lib/Horde/SessionHandler/Storage/File.php | 22 ++-- .../SessionHandler/Storage/Hashtable.php | 27 ++-- lib/Horde/SessionHandler/Storage/Memcache.php | 19 +-- lib/Horde/SessionHandler/Storage/Mongo.php | 115 +++++++++--------- lib/Horde/SessionHandler/Storage/Sql.php | 69 ++++++----- lib/Horde/SessionHandler/Storage/Stack.php | 7 +- .../1_horde_sessionhandler_base_tables.php | 11 +- ...2_horde_sessionhandler_fix_blob_length.php | 5 +- test/Horde/SessionHandler/AllTests.php | 3 +- test/Horde/SessionHandler/Storage/Base.php | 24 ++-- .../SessionHandler/Storage/BaseTestCase.php | 26 ++-- .../SessionHandler/Storage/BuiltinTest.php | 40 +++--- .../SessionHandler/Storage/ExternalTest.php | 25 ++-- .../Horde/SessionHandler/Storage/FileTest.php | 10 +- .../SessionHandler/Storage/MemcacheTest.php | 14 ++- .../SessionHandler/Storage/MongoTest.php | 21 ++-- .../SessionHandler/Storage/Sql/MysqlTest.php | 11 +- .../SessionHandler/Storage/Sql/MysqliTest.php | 11 +- .../SessionHandler/Storage/Sql/Oci8Test.php | 11 +- .../Storage/Sql/Pdo/MysqlTest.php | 18 ++- .../Storage/Sql/Pdo/PgsqlTest.php | 18 ++- .../Storage/Sql/Pdo/SqliteTest.php | 14 ++- .../Storage/Sql/SqlBaseTestCase.php | 21 ++-- .../SessionHandler/Storage/StackTest.php | 34 +++--- test/Horde/SessionHandler/bootstrap.php | 1 + 30 files changed, 383 insertions(+), 275 deletions(-) diff --git a/lib/Horde/SessionHandler.php b/lib/Horde/SessionHandler.php index ce514ad..eb69999 100644 --- a/lib/Horde/SessionHandler.php +++ b/lib/Horde/SessionHandler.php @@ -1,8 +1,9 @@ */ - public function __construct(Horde_SessionHandler_Storage $storage, - array $params = array()) - { + public function __construct( + Horde_SessionHandler_Storage $storage, + array $params = [] + ) { $params = array_merge($this->_params, $params); - $this->_logger = isset($params['logger']) - ? $params['logger'] - : new Horde_Support_Stub(); + $this->_logger = $params['logger'] + ?? new Horde_Support_Stub(); unset($params['logger']); $this->_params = $params; @@ -205,9 +206,9 @@ public function read($id): string|false */ public function write($id, $session_data): bool { - if ($this->changed || - (empty($this->_params['no_md5']) && - ($this->_sig != md5($session_data)))) { + if ($this->changed + || (empty($this->_params['no_md5']) + && ($this->_sig != md5($session_data)))) { if (!$this->_storage->write($id, $session_data)) { $this->_logger->log('Failed to write session data (' . $id . ')', 'DEBUG'); return false; @@ -273,10 +274,10 @@ public function getSessionIDs() */ public function getSessionsInfo() { - $info = array(); + $info = []; - if (empty($this->_params['parse']) || - !is_callable($this->_params['parse'])) { + if (empty($this->_params['parse']) + || !is_callable($this->_params['parse'])) { return $info; } diff --git a/lib/Horde/SessionHandler/Exception.php b/lib/Horde/SessionHandler/Exception.php index 2eeb1f7..ea6e104 100644 --- a/lib/Horde/SessionHandler/Exception.php +++ b/lib/Horde/SessionHandler/Exception.php @@ -1,8 +1,9 @@ _params = array_merge($this->_params, $params); } @@ -61,9 +62,7 @@ public function __sleep() * * @deprecated */ - public function setLogger(Horde_Log_Logger $log) - { - } + public function setLogger(Horde_Log_Logger $log) {} /** * Open the backend. diff --git a/lib/Horde/SessionHandler/Storage/Builtin.php b/lib/Horde/SessionHandler/Storage/Builtin.php index 222ac0e..456cbae 100644 --- a/lib/Horde/SessionHandler/Storage/Builtin.php +++ b/lib/Horde/SessionHandler/Storage/Builtin.php @@ -1,10 +1,11 @@ isFile() && - ($val->getFilename() == 'sess_' . $id)) { + if ($val->isFile() + && ($val->getFilename() == 'sess_' . $id)) { return unlink($val->getPathname()); } } @@ -93,7 +90,7 @@ public function gc($maxlifetime = 300) */ public function getSessionIDs() { - $sessions = array(); + $sessions = []; try { $di = new DirectoryIterator($this->_path); @@ -103,8 +100,8 @@ public function getSessionIDs() foreach ($di as $val) { /* Make sure we're dealing with files that start with sess_. */ - if ($val->isFile() && - (strpos($val->getFilename(), 'sess_') === 0)) { + if ($val->isFile() + && (strpos($val->getFilename(), 'sess_') === 0)) { $sessions[] = substr($val->getFilename(), strlen('sess_')); } } diff --git a/lib/Horde/SessionHandler/Storage/External.php b/lib/Horde/SessionHandler/Storage/External.php index abede6a..603ebee 100644 --- a/lib/Horde/SessionHandler/Storage/External.php +++ b/lib/Horde/SessionHandler/Storage/External.php @@ -1,9 +1,10 @@ isFile() && - (strpos($val->getFilename(), self::PREFIX) === 0) && - ($val->getMTime() < $expire_time)) { + if ($val->isFile() + && (strpos($val->getFilename(), self::PREFIX) === 0) + && ($val->getMTime() < $expire_time)) { @unlink($val->getPathname()); } } @@ -166,17 +167,18 @@ public function gc($maxlifetime = 300) */ public function getSessionIDs() { - $ids = array(); + $ids = []; try { $di = new DirectoryIterator($this->_params['path']); foreach ($di as $val) { - if ($val->isFile() && - (strpos($val->getFilename(), self::PREFIX) === 0)) { + if ($val->isFile() + && (strpos($val->getFilename(), self::PREFIX) === 0)) { $ids[] = substr($val->getFilename(), strlen(self::PREFIX)); } } - } catch (UnexpectedValueException $e) {} + } catch (UnexpectedValueException $e) { + } return $ids; } diff --git a/lib/Horde/SessionHandler/Storage/Hashtable.php b/lib/Horde/SessionHandler/Storage/Hashtable.php index c6a5192..9b25207 100644 --- a/lib/Horde/SessionHandler/Storage/Hashtable.php +++ b/lib/Horde/SessionHandler/Storage/Hashtable.php @@ -1,6 +1,7 @@ */ - public function __construct(array $params = array()) + public function __construct(array $params = []) { if (empty($params['hashtable'])) { throw new InvalidArgumentException('Missing hashtable parameter.'); @@ -69,15 +70,13 @@ public function __construct(array $params = array()) parent::__construct($params); if (!empty($this->_params['track']) && (!rand(0, 999))) { - register_shutdown_function(array($this, 'trackGC')); + register_shutdown_function([$this, 'trackGC']); } } /** */ - public function open($save_path = null, $session_name = null) - { - } + public function open($save_path = null, $session_name = null) {} /** */ @@ -113,17 +112,17 @@ public function read($id) */ public function write($id, $session_data) { - $base = array_filter(array( - 'timeout' => ini_get('session.gc_maxlifetime') - )); + $base = array_filter([ + 'timeout' => ini_get('session.gc_maxlifetime'), + ]); if (!empty($this->_params['track'])) { // Do a replace - the only time it should fail is if we are // writing a session for the first time. If that is the case, // update the session tracker. - $res = $this->_hash->set($id, $session_data, array_merge($base, array( + $res = $this->_hash->set($id, $session_data, array_merge($base, [ 'replace' => true, - ))); + ])); $track = !$res; } else { $res = $track = false; @@ -222,9 +221,9 @@ public function trackGC() */ protected function _getTrackIds() { - if ((($ids = $this->_hash->get($this->_trackID)) === false) || - !($ids = json_decode($ids, true))) { - $ids = array(); + if ((($ids = $this->_hash->get($this->_trackID)) === false) + || !($ids = json_decode($ids, true))) { + $ids = []; } return $ids; diff --git a/lib/Horde/SessionHandler/Storage/Memcache.php b/lib/Horde/SessionHandler/Storage/Memcache.php index dba9a36..782d9a3 100644 --- a/lib/Horde/SessionHandler/Storage/Memcache.php +++ b/lib/Horde/SessionHandler/Storage/Memcache.php @@ -1,6 +1,7 @@ _params['track_lifetime'] = ini_get('session.gc_maxlifetime'); } - if (!empty($this->_params['track']) && - (substr(time(), -3) === '000')) { - register_shutdown_function(array($this, 'trackGC')); + if (!empty($this->_params['track']) + && (substr(time(), -3) === '000')) { + register_shutdown_function([$this, 'trackGC']); } } @@ -132,8 +133,8 @@ public function write($id, $session_data) $res = $track = false; } - if (!$res && - !$this->_memcache->set($id, $session_data)) { + if (!$res + && !$this->_memcache->set($id, $session_data)) { return false; } @@ -141,7 +142,7 @@ public function write($id, $session_data) $this->_memcache->lock($this->_trackID); $ids = $this->_memcache->get($this->_trackID); if ($ids === false) { - $ids = array(); + $ids = []; } $ids[$id] = time(); @@ -197,7 +198,7 @@ public function getSessionIDs() $ids = $this->_memcache->get($this->_trackID); return ($ids === false) - ? array() + ? [] : array_keys($ids); } diff --git a/lib/Horde/SessionHandler/Storage/Mongo.php b/lib/Horde/SessionHandler/Storage/Mongo.php index 88780ad..e5ad329 100644 --- a/lib/Horde/SessionHandler/Storage/Mongo.php +++ b/lib/Horde/SessionHandler/Storage/Mongo.php @@ -1,6 +1,7 @@ array( - self::MODIFIED => 1 - ) - ); + protected $_indices = [ + 'index_ts' => [ + self::MODIFIED => 1, + ], + ]; /** * Constructor. @@ -62,15 +63,15 @@ class Horde_SessionHandler_Storage_Mongo extends Horde_SessionHandler_Storage im * mongo_db: (Horde_Mongo_Client) [REQUIRED] The Mongo client object. * */ - public function __construct(array $params = array()) + public function __construct(array $params = []) { if (!isset($params['mongo_db'])) { throw new InvalidArgumentException('Missing mongo_db parameter.'); } - parent::__construct(array_merge(array( - 'collection' => 'horde_sessionhandler' - ), $params)); + parent::__construct(array_merge([ + 'collection' => 'horde_sessionhandler', + ], $params)); $this->_db = $this->_params['mongo_db']->selectCollection(null, $this->_params['collection']); } @@ -89,10 +90,11 @@ public function close() if ($this->_locked) { try { $this->_db->update( - array(self::SID => $this->_locked), - array('$unset' => array(self::LOCK => '')) + [self::SID => $this->_locked], + ['$unset' => [self::LOCK => '']] ); - } catch (MongoException $e) {} + } catch (MongoException $e) { + } $this->_locked = false; } @@ -107,7 +109,7 @@ public function read($id) * we need findAndModify() for its atomicity for locking, but this * atomicity means we can't tell the difference between a * non-existent session and a locked session. */ - $exists = $this->_db->count(array(self::SID => $id)); + $exists = $this->_db->count([self::SID => $id]); $exist_check = false; $i = 0; @@ -116,10 +118,10 @@ public function read($id) $max = ini_get('max_execution_time') * 10; while (true) { - $data = array( + $data = [ self::LOCK => time(), - self::SID => $id - ); + self::SID => $id, + ]; /* This call will either create the session if it doesn't exist, * or will update the current session and lock it if not already @@ -127,15 +129,15 @@ public function read($id) * an empty set and we need to sleep and wait for lock to be * removed. */ $res = $this->_db->findAndModify( - array( + [ self::SID => $id, - self::LOCK => array('$exists' => $exist_check) - ), - array('$set' => $data), - array(self::DATA => true), - array( - 'upsert' => !$exists - ) + self::LOCK => ['$exists' => $exist_check], + ], + ['$set' => $data], + [self::DATA => true], + [ + 'upsert' => !$exists, + ] ); if (!$exists || isset($res[self::DATA])) { @@ -147,8 +149,8 @@ public function read($id) * process. */ if ($i == 10) { $res = $this->_db->findOne( - array(self::SID => $id), - array(self::LOCK => true) + [self::SID => $id], + [self::LOCK => true] ); $max = isset($res[self::LOCK]) @@ -177,15 +179,15 @@ public function write($id, $session_data) { /* Update/insert session data. */ try { - $this->_db->update(array( - self::SID => $id - ), array( + $this->_db->update([ + self::SID => $id, + ], [ self::DATA => new MongoBinData($session_data, MongoBinData::BYTE_ARRAY), self::MODIFIED => time(), - self::SID => $id - ), array( - 'upsert' => true - )); + self::SID => $id, + ], [ + 'upsert' => true, + ]); $this->_locked = false; } catch (MongoException $e) { @@ -200,11 +202,12 @@ public function write($id, $session_data) public function destroy($id) { try { - $this->_db->remove(array( - self::SID => $id - )); + $this->_db->remove([ + self::SID => $id, + ]); return true; - } catch (MongoException $e) {} + } catch (MongoException $e) { + } return false; } @@ -214,13 +217,14 @@ public function destroy($id) public function gc($maxlifetime = 300) { try { - $this->_db->remove(array( - self::MODIFIED => array( - '$lt' => (time() - $maxlifetime) - ) - )); + $this->_db->remove([ + self::MODIFIED => [ + '$lt' => (time() - $maxlifetime), + ], + ]); return true; - } catch (MongoException $e) {} + } catch (MongoException $e) { + } return false; } @@ -229,19 +233,20 @@ public function gc($maxlifetime = 300) */ public function getSessionIDs() { - $ids = array(); + $ids = []; try { - $cursor = $this->_db->find(array( - self::MODIFIED => array( - '$gte' => (time() - ini_get('session.gc_maxlifetime')) - ) - ), array(self::SID => true)); + $cursor = $this->_db->find([ + self::MODIFIED => [ + '$gte' => (time() - ini_get('session.gc_maxlifetime')), + ], + ], [self::SID => true]); foreach ($cursor as $val) { $ids[] = $val[self::SID]; } - } catch (MongoException $e) {} + } catch (MongoException $e) { + } return $ids; } diff --git a/lib/Horde/SessionHandler/Storage/Sql.php b/lib/Horde/SessionHandler/Storage/Sql.php index e2afb56..44f8f99 100644 --- a/lib/Horde/SessionHandler/Storage/Sql.php +++ b/lib/Horde/SessionHandler/Storage/Sql.php @@ -1,4 +1,5 @@ * - * Copyright 2002-2017 Horde LLC (http://www.horde.org/) + * Copyright 2002-2026 Horde LLC (http://www.horde.org/) * * See the enclosed file LICENSE for license information (LGPL). If you * did not receive this file, see http://www.horde.org/licenses/lgpl21. @@ -48,7 +49,7 @@ class Horde_SessionHandler_Storage_Sql extends Horde_SessionHandler_Storage * * @throws InvalidArgumentException */ - public function __construct(array $params = array()) + public function __construct(array $params = []) { if (!isset($params['db'])) { throw new InvalidArgumentException('Missing db parameter.'); @@ -56,9 +57,9 @@ public function __construct(array $params = array()) $this->_db = $params['db']; unset($params['db']); - parent::__construct(array_merge(array( - 'table' => 'horde_sessionhandler' - ), $params)); + parent::__construct(array_merge([ + 'table' => 'horde_sessionhandler', + ], $params)); } /** @@ -94,15 +95,18 @@ public function read($id) } /* Build query. */ - $query = sprintf('SELECT session_data FROM %s WHERE session_id = ?', - $this->_params['table']); - $values = array($id); + $query = sprintf( + 'SELECT session_data FROM %s WHERE session_id = ?', + $this->_params['table'] + ); + $values = [$id]; /* Execute the query. */ try { $columns = $this->_db->columns($this->_params['table']); return $columns['session_data']->binaryToString( - $this->_db->selectValue($query, $values)); + $this->_db->selectValue($query, $values) + ); } catch (Horde_Db_Exception $e) { return ''; } @@ -120,29 +124,32 @@ public function write($id, $session_data) /* Check if session exists. */ try { $exists = $this->_db->selectValue( - sprintf('SELECT 1 FROM %s WHERE session_id = ?', - $this->_params['table']), - array($id)); + sprintf( + 'SELECT 1 FROM %s WHERE session_id = ?', + $this->_params['table'] + ), + [$id] + ); } catch (Horde_Db_Exception $e) { return false; } /* Update or insert session data. */ - $values = array( + $values = [ 'session_data' => new Horde_Db_Value_Binary($session_data), - 'session_lastmodified' => time() - ); + 'session_lastmodified' => time(), + ]; try { if ($exists) { $this->_db->updateBlob( $this->_params['table'], $values, - array('session_id = ?', array($id)) + ['session_id = ?', [$id]] ); } else { $this->_db->insertBlob( $this->_params['table'], - array_merge(array('session_id' => $id), $values), + array_merge(['session_id' => $id], $values), null, $id ); @@ -164,9 +171,11 @@ public function write($id, $session_data) public function destroy($id) { /* Build the SQL query. */ - $query = sprintf('DELETE FROM %s WHERE session_id = ?', - $this->_params['table']); - $values = array($id); + $query = sprintf( + 'DELETE FROM %s WHERE session_id = ?', + $this->_params['table'] + ); + $values = [$id]; /* Execute the query. */ try { @@ -184,9 +193,11 @@ public function destroy($id) public function gc($maxlifetime = 300) { /* Build the SQL query. */ - $query = sprintf('DELETE FROM %s WHERE session_lastmodified < ?', - $this->_params['table']); - $values = array(time() - $maxlifetime); + $query = sprintf( + 'DELETE FROM %s WHERE session_lastmodified < ?', + $this->_params['table'] + ); + $values = [time() - $maxlifetime]; /* Execute the query. */ try { @@ -205,16 +216,18 @@ public function getSessionIDs() $this->open(); /* Build the SQL query. */ - $query = sprintf('SELECT session_id FROM %s' . - ' WHERE session_lastmodified >= ?', - $this->_params['table']); - $values = array(time() - ini_get('session.gc_maxlifetime')); + $query = sprintf( + 'SELECT session_id FROM %s' + . ' WHERE session_lastmodified >= ?', + $this->_params['table'] + ); + $values = [time() - ini_get('session.gc_maxlifetime')]; /* Execute the query. */ try { return $this->_db->selectValues($query, $values); } catch (Horde_Db_Exception $e) { - return array(); + return []; } } } diff --git a/lib/Horde/SessionHandler/Storage/Stack.php b/lib/Horde/SessionHandler/Storage/Stack.php index dea4437..49388cf 100644 --- a/lib/Horde/SessionHandler/Storage/Stack.php +++ b/lib/Horde/SessionHandler/Storage/Stack.php @@ -1,11 +1,12 @@ tables())) { - $t = $this->createTable('horde_sessionhandler', array('autoincrementKey' => false)); - $t->column('session_id', 'string', array('limit' => 32, 'null' => false)); - $t->column('session_lastmodified', 'integer', array('null' => false)); + $t = $this->createTable('horde_sessionhandler', ['autoincrementKey' => false]); + $t->column('session_id', 'string', ['limit' => 32, 'null' => false]); + $t->column('session_lastmodified', 'integer', ['null' => false]); $t->column('session_data', 'binary'); - $t->primaryKey(array('session_id')); + $t->primaryKey(['session_id']); $t->end(); - $this->addIndex('horde_sessionhandler', array('session_lastmodified')); + $this->addIndex('horde_sessionhandler', ['session_lastmodified']); } } diff --git a/migration/Horde/SessionHandler/2_horde_sessionhandler_fix_blob_length.php b/migration/Horde/SessionHandler/2_horde_sessionhandler_fix_blob_length.php index e09a8b6..2169234 100644 --- a/migration/Horde/SessionHandler/2_horde_sessionhandler_fix_blob_length.php +++ b/migration/Horde/SessionHandler/2_horde_sessionhandler_fix_blob_length.php @@ -1,4 +1,5 @@ changeColumn('horde_sessionhandler', 'session_data', 'binary'); } - public function down() - { - } + public function down() {} } diff --git a/test/Horde/SessionHandler/AllTests.php b/test/Horde/SessionHandler/AllTests.php index 254a952..6ade172 100644 --- a/test/Horde/SessionHandler/AllTests.php +++ b/test/Horde/SessionHandler/AllTests.php @@ -1,5 +1,6 @@ run(); diff --git a/test/Horde/SessionHandler/Storage/Base.php b/test/Horde/SessionHandler/Storage/Base.php index 6015382..bcb7a78 100644 --- a/test/Horde/SessionHandler/Storage/Base.php +++ b/test/Horde/SessionHandler/Storage/Base.php @@ -1,4 +1,5 @@ * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 @@ -6,9 +7,14 @@ * @package Horde_SessionHandler * @subpackage UnitTests */ + namespace Horde\SessionHandler\Storage; -use \Horde_Test_Case as TestCase; +use Horde_Test_Case as TestCase; + +/** + * @coversNothing + */ class BaseTestCase extends TestCase { protected static $handler; @@ -43,14 +49,14 @@ protected function _list() /* List while session is active. */ $ids = self::$handler->getSessionIDs(); sort($ids); - $this->assertEquals(array('sessionid', 'sessionid2'), $ids); + $this->assertEquals(['sessionid', 'sessionid2'], $ids); $this->assertTrue(self::$handler->close()); /* List while session is inactive. */ $this->assertTrue(self::$handler->open(self::$dir, 'sessionname')); $ids = self::$handler->getSessionIDs(); sort($ids); - $this->assertEquals(array('sessionid', 'sessionid2'), $ids); + $this->assertEquals(['sessionid', 'sessionid2'], $ids); $this->assertTrue(self::$handler->close()); } @@ -59,16 +65,20 @@ protected function _destroy() $this->assertTrue(self::$handler->open(self::$dir, 'sessionname')); self::$handler->read('sessionid2'); $this->assertTrue(self::$handler->destroy('sessionid2')); - $this->assertEquals(array('sessionid'), - self::$handler->getSessionIDs()); + $this->assertEquals( + ['sessionid'], + self::$handler->getSessionIDs() + ); } protected function _gc() { $this->assertTrue(self::$handler->open(self::$dir, 'sessionname')); $this->assertTrue(self::$handler->gc(-1)); - $this->assertEquals(array(), - self::$handler->getSessionIDs()); + $this->assertEquals( + [], + self::$handler->getSessionIDs() + ); } public static function setUpBeforeClass(): void diff --git a/test/Horde/SessionHandler/Storage/BaseTestCase.php b/test/Horde/SessionHandler/Storage/BaseTestCase.php index 973e3fc..7b0ff78 100644 --- a/test/Horde/SessionHandler/Storage/BaseTestCase.php +++ b/test/Horde/SessionHandler/Storage/BaseTestCase.php @@ -1,4 +1,5 @@ * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 @@ -6,10 +7,15 @@ * @package Horde_SessionHandler * @subpackage UnitTests */ + namespace Horde\SessionHandler\Storage; -use \Horde_Test_Case as TestCase; -use \Horde_Util; +use Horde_Test_Case as TestCase; +use Horde_Util; + +/** + * @coversNothing + */ class BaseTestCase extends TestCase { protected static $handler; @@ -44,14 +50,14 @@ protected function _list() /* List while session is active. */ $ids = self::$handler->getSessionIDs(); sort($ids); - $this->assertEquals(array('sessionid', 'sessionid2'), $ids); + $this->assertEquals(['sessionid', 'sessionid2'], $ids); $this->assertTrue(self::$handler->close()); /* List while session is inactive. */ $this->assertTrue(self::$handler->open(self::$dir, 'sessionname')); $ids = self::$handler->getSessionIDs(); sort($ids); - $this->assertEquals(array('sessionid', 'sessionid2'), $ids); + $this->assertEquals(['sessionid', 'sessionid2'], $ids); $this->assertTrue(self::$handler->close()); } @@ -60,16 +66,20 @@ protected function _destroy() $this->assertTrue(self::$handler->open(self::$dir, 'sessionname')); self::$handler->read('sessionid2'); $this->assertTrue(self::$handler->destroy('sessionid2')); - $this->assertEquals(array('sessionid'), - self::$handler->getSessionIDs()); + $this->assertEquals( + ['sessionid'], + self::$handler->getSessionIDs() + ); } protected function _gc() { $this->assertTrue(self::$handler->open(self::$dir, 'sessionname')); $this->assertTrue(self::$handler->gc(-1)); - $this->assertEquals(array(), - self::$handler->getSessionIDs()); + $this->assertEquals( + [], + self::$handler->getSessionIDs() + ); } public static function setUpBeforeClass(): void diff --git a/test/Horde/SessionHandler/Storage/BuiltinTest.php b/test/Horde/SessionHandler/Storage/BuiltinTest.php index 65ccd4f..6c2fce6 100644 --- a/test/Horde/SessionHandler/Storage/BuiltinTest.php +++ b/test/Horde/SessionHandler/Storage/BuiltinTest.php @@ -1,18 +1,22 @@ * @category Horde * @package Horde_SessionHandler * @subpackage UnitTests * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 + * @coversNothing */ class BuiltinTest extends BaseTestCase { @@ -61,13 +65,13 @@ public function testList() /* List while session is active. */ $ids = self::$handler->getSessionIDs(); sort($ids); - $this->assertEquals(array('sessionid', 'sessionid2'), $ids); + $this->assertEquals(['sessionid', 'sessionid2'], $ids); session_write_close(); /* List while session is inactive. */ $ids = self::$handler->getSessionIDs(); sort($ids); - $this->assertEquals(array('sessionid', 'sessionid2'), $ids); + $this->assertEquals(['sessionid', 'sessionid2'], $ids); } /** @@ -81,11 +85,15 @@ public function testDestroy() session_start(); $sessionIds = self::$handler->getSessionIDs(); sort($sessionIds); - $this->assertEquals(array('sessionid', 'sessionid2'), - $sessionIds); + $this->assertEquals( + ['sessionid', 'sessionid2'], + $sessionIds + ); session_destroy(); - $this->assertEquals(array('sessionid'), - self::$handler->getSessionIDs()); + $this->assertEquals( + ['sessionid'], + self::$handler->getSessionIDs() + ); } /** @@ -102,8 +110,10 @@ public function testGc() ini_set('session.gc_maxlifetime', -1); session_name('sessionname'); session_start(); - $this->assertEquals(array(), - self::$handler->getSessionIDs()); + $this->assertEquals( + [], + self::$handler->getSessionIDs() + ); } protected function _write() @@ -124,7 +134,7 @@ public static function setUpBeforeClass(): void ini_set('session.use_cookies', 0); ini_set('session.save_path', self::$dir); } - self::$handler = new Horde_SessionHandler_Storage_Builtin(array('path' => self::$dir)); + self::$handler = new Horde_SessionHandler_Storage_Builtin(['path' => self::$dir]); } public function tearDown(): void @@ -143,10 +153,10 @@ public static function tearDownAfterClass(): void { parent::tearDownAfterClass(); unset($_SESSION); - if ((function_exists('session_status') && - session_status() == PHP_SESSION_ACTIVE) || - (!function_exists('session_status') && - session_id())) { + if ((function_exists('session_status') + && session_status() == PHP_SESSION_ACTIVE) + || (!function_exists('session_status') + && session_id())) { session_destroy(); } if (!headers_sent()) { diff --git a/test/Horde/SessionHandler/Storage/ExternalTest.php b/test/Horde/SessionHandler/Storage/ExternalTest.php index 10fa5bc..56d5c18 100644 --- a/test/Horde/SessionHandler/Storage/ExternalTest.php +++ b/test/Horde/SessionHandler/Storage/ExternalTest.php @@ -1,19 +1,23 @@ * @category Horde * @package Horde_SessionHandler * @subpackage UnitTests * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 + * @coversNothing */ class ExternalTest extends BaseTestCase { @@ -86,13 +90,14 @@ public function testGc() public static function setUpBeforeClass(): void { parent::setUpBeforeClass(); - $external = new Horde_SessionHandler_Storage_File(array('path' => self::$dir)); + $external = new Horde_SessionHandler_Storage_File(['path' => self::$dir]); self::$handler = new Horde_SessionHandler_Storage_External( - array('open' => array($external, 'open'), - 'close' => array($external, 'close'), - 'read' => array($external, 'read'), - 'write' => array($external, 'write'), - 'destroy' => array($external, 'destroy'), - 'gc' => array($external, 'gc'))); + ['open' => [$external, 'open'], + 'close' => [$external, 'close'], + 'read' => [$external, 'read'], + 'write' => [$external, 'write'], + 'destroy' => [$external, 'destroy'], + 'gc' => [$external, 'gc']] + ); } } diff --git a/test/Horde/SessionHandler/Storage/FileTest.php b/test/Horde/SessionHandler/Storage/FileTest.php index ef1c4fd..4521730 100644 --- a/test/Horde/SessionHandler/Storage/FileTest.php +++ b/test/Horde/SessionHandler/Storage/FileTest.php @@ -1,18 +1,22 @@ * @category Horde * @package Horde_SessionHandler * @subpackage UnitTests * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 + * @coversNothing */ class FileTest extends BaseTestCase { @@ -64,6 +68,6 @@ public function testGc() public static function setUpBeforeClass(): void { parent::setUpBeforeClass(); - self::$handler = new Horde_SessionHandler_Storage_File(array('path' => self::$dir)); + self::$handler = new Horde_SessionHandler_Storage_File(['path' => self::$dir]); } } diff --git a/test/Horde/SessionHandler/Storage/MemcacheTest.php b/test/Horde/SessionHandler/Storage/MemcacheTest.php index 4e8643f..13d39e9 100644 --- a/test/Horde/SessionHandler/Storage/MemcacheTest.php +++ b/test/Horde/SessionHandler/Storage/MemcacheTest.php @@ -1,17 +1,20 @@ * @category Horde * @package Horde_SessionHandler * @subpackage UnitTests * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 + * @coversNothing */ class MemcacheTest extends BaseTestCase { @@ -60,8 +63,10 @@ public static function setUpBeforeClass(): void self::$reason = 'No memcache extension.'; return; } - $config = self::getConfig('SESSIONHANDLER_MEMCACHE_TEST_CONFIG', - dirname(__FILE__) . '/..'); + $config = self::getConfig( + 'SESSIONHANDLER_MEMCACHE_TEST_CONFIG', + dirname(__FILE__) . '/..' + ); if (!$config || empty($config['sessionhandler']['memcache'])) { self::$reason = 'No memcache configuration.'; return; @@ -70,7 +75,8 @@ public static function setUpBeforeClass(): void $memcache->delete('sessionid'); $memcache->delete('sessionid2'); self::$handler = new Horde_SessionHandler_Storage_Memcache( - array('memcache' => $memcache, 'track' => true)); + ['memcache' => $memcache, 'track' => true] + ); parent::setUpBeforeClass(); } diff --git a/test/Horde/SessionHandler/Storage/MongoTest.php b/test/Horde/SessionHandler/Storage/MongoTest.php index 8005951..78d02a0 100644 --- a/test/Horde/SessionHandler/Storage/MongoTest.php +++ b/test/Horde/SessionHandler/Storage/MongoTest.php @@ -1,17 +1,20 @@ * @category Horde * @package Horde_SessionHandler * @subpackage UnitTests * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 + * @coversNothing */ class MongoTest extends BaseTestCase { @@ -57,21 +60,21 @@ public function testDestroy() public static function setUpBeforeClass(): void { - if (($config = self::getConfig('SESSIONHANDLER_MONGO_TEST_CONFIG', __DIR__ . '/..')) && - isset($config['sessionhandler']['mongo'])) { + if (($config = self::getConfig('SESSIONHANDLER_MONGO_TEST_CONFIG', __DIR__ . '/..')) + && isset($config['sessionhandler']['mongo'])) { $factory = new Horde_Test_Factory_Mongo(); - self::$mongo = $factory->create(array( + self::$mongo = $factory->create([ 'config' => $config['sessionhandler']['mongo'], - 'dbname' => 'horde_sessionhandler_test' - )); + 'dbname' => 'horde_sessionhandler_test', + ]); } if (empty(self::$mongo)) { self::$reason = 'MongoDB not available.'; return; } - self::$handler = new Horde_SessionHandler_Storage_Mongo(array( - 'mongo_db' => self::$mongo - )); + self::$handler = new Horde_SessionHandler_Storage_Mongo([ + 'mongo_db' => self::$mongo, + ]); parent::setUpBeforeClass(); } diff --git a/test/Horde/SessionHandler/Storage/Sql/MysqlTest.php b/test/Horde/SessionHandler/Storage/Sql/MysqlTest.php index ca68200..4fd250a 100644 --- a/test/Horde/SessionHandler/Storage/Sql/MysqlTest.php +++ b/test/Horde/SessionHandler/Storage/Sql/MysqlTest.php @@ -1,17 +1,20 @@ * @category Horde * @package SessionHandler * @subpackage UnitTests * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 + * @coversNothing */ class MysqlTest extends SqlBaseTestCase { @@ -21,8 +24,10 @@ public static function setUpBeforeClass(): void self::$reason = 'No mysql extension'; return; } - $config = self::getConfig('SESSIONHANDLER_SQL_MYSQL_TEST_CONFIG', - dirname(__FILE__) . '/../..'); + $config = self::getConfig( + 'SESSIONHANDLER_SQL_MYSQL_TEST_CONFIG', + dirname(__FILE__) . '/../..' + ); if (!$config || empty($config['sessionhandler']['sql']['mysql'])) { self::$reason = 'No mysql configuration'; return; diff --git a/test/Horde/SessionHandler/Storage/Sql/MysqliTest.php b/test/Horde/SessionHandler/Storage/Sql/MysqliTest.php index a243fe6..254dae1 100644 --- a/test/Horde/SessionHandler/Storage/Sql/MysqliTest.php +++ b/test/Horde/SessionHandler/Storage/Sql/MysqliTest.php @@ -1,17 +1,20 @@ * @category Horde * @package SessionHandler * @subpackage UnitTests * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 + * @coversNothing */ class MysqliTest extends SqlBaseTestCase { @@ -21,8 +24,10 @@ public static function setUpBeforeClass(): void self::$reason = 'No mysqli extension'; return; } - $config = self::getConfig('SESSIONHANDLER_SQL_MYSQLI_TEST_CONFIG', - dirname(__FILE__) . '/../..'); + $config = self::getConfig( + 'SESSIONHANDLER_SQL_MYSQLI_TEST_CONFIG', + dirname(__FILE__) . '/../..' + ); if (!$config || empty($config['sessionhandler']['sql']['mysqli'])) { self::$reason = 'No mysqli configuration'; return; diff --git a/test/Horde/SessionHandler/Storage/Sql/Oci8Test.php b/test/Horde/SessionHandler/Storage/Sql/Oci8Test.php index f582318..9ec74a5 100644 --- a/test/Horde/SessionHandler/Storage/Sql/Oci8Test.php +++ b/test/Horde/SessionHandler/Storage/Sql/Oci8Test.php @@ -1,17 +1,20 @@ * @category Horde * @package SessionHandler * @subpackage UnitTests * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 + * @coversNothing */ class Oci8Test extends SqlBaseTestCase { @@ -21,8 +24,10 @@ public static function setUpBeforeClass(): void self::$reason = 'No oci8 extension'; return; } - $config = self::getConfig('SESSIONHANDLER_SQL_OCI8_TEST_CONFIG', - dirname(__FILE__) . '/../..'); + $config = self::getConfig( + 'SESSIONHANDLER_SQL_OCI8_TEST_CONFIG', + dirname(__FILE__) . '/../..' + ); if (!$config || empty($config['sessionhandler']['sql']['oci8'])) { self::$reason = 'No oci8 configuration'; return; diff --git a/test/Horde/SessionHandler/Storage/Sql/Pdo/MysqlTest.php b/test/Horde/SessionHandler/Storage/Sql/Pdo/MysqlTest.php index 245059a..f9f6eb7 100644 --- a/test/Horde/SessionHandler/Storage/Sql/Pdo/MysqlTest.php +++ b/test/Horde/SessionHandler/Storage/Sql/Pdo/MysqlTest.php @@ -1,31 +1,37 @@ * @category Horde * @package SessionHandler * @subpackage UnitTests * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 + * @coversNothing */ class MysqlTest extends SqlBaseTestCase { public static function setUpBeforeClass(): void { - if (!extension_loaded('pdo') || - !in_array('mysql', PDO::getAvailableDrivers())) { + if (!extension_loaded('pdo') + || !in_array('mysql', PDO::getAvailableDrivers())) { self::$reason = 'No mysql extension or no mysql PDO driver'; return; } - $config = self::getConfig('SESSIONHANDLER_SQL_PDO_MYSQL_TEST_CONFIG', - dirname(__FILE__) . '/../../..'); + $config = self::getConfig( + 'SESSIONHANDLER_SQL_PDO_MYSQL_TEST_CONFIG', + dirname(__FILE__) . '/../../..' + ); if ($config && !empty($config['sessionhandler']['sql']['pdo_mysql'])) { self::$db = new Horde_Db_Adapter_Pdo_Mysql($config['sessionhandler']['sql']['pdo_mysql']); parent::setUpBeforeClass(); diff --git a/test/Horde/SessionHandler/Storage/Sql/Pdo/PgsqlTest.php b/test/Horde/SessionHandler/Storage/Sql/Pdo/PgsqlTest.php index f7895ef..4512615 100644 --- a/test/Horde/SessionHandler/Storage/Sql/Pdo/PgsqlTest.php +++ b/test/Horde/SessionHandler/Storage/Sql/Pdo/PgsqlTest.php @@ -1,31 +1,37 @@ * @category Horde * @package SessionHandler * @subpackage UnitTests * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 + * @coversNothing */ class PgsqlTest extends SqlBaseTestCase { public static function setUpBeforeClass(): void { - if (!extension_loaded('pdo') || - !in_array('pgsql', PDO::getAvailableDrivers())) { + if (!extension_loaded('pdo') + || !in_array('pgsql', PDO::getAvailableDrivers())) { self::$reason = 'No pgsql extension or no pgsql PDO driver'; return; } - $config = self::getConfig('SESSIONHANDLER_SQL_PDO_PGSQL_TEST_CONFIG', - dirname(__FILE__) . '/../../..'); + $config = self::getConfig( + 'SESSIONHANDLER_SQL_PDO_PGSQL_TEST_CONFIG', + dirname(__FILE__) . '/../../..' + ); if ($config && !empty($config['sessionhandler']['sql']['pdo_pgsql'])) { self::$db = new Horde_Db_Adapter_Pdo_Pgsql($config['sessionhandler']['sql']['pdo_pgsql']); parent::setUpBeforeClass(); diff --git a/test/Horde/SessionHandler/Storage/Sql/Pdo/SqliteTest.php b/test/Horde/SessionHandler/Storage/Sql/Pdo/SqliteTest.php index 92b334c..cd4bfa9 100644 --- a/test/Horde/SessionHandler/Storage/Sql/Pdo/SqliteTest.php +++ b/test/Horde/SessionHandler/Storage/Sql/Pdo/SqliteTest.php @@ -1,21 +1,25 @@ * @category Horde * @package SessionHandler * @subpackage UnitTests * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 + * @coversNothing */ class SqliteTest extends SqlBaseTestCase { @@ -28,6 +32,6 @@ public static function setUpBeforeClass(): void parent::setUpBeforeClass(); } catch (Horde_Test_Exception $e) { self::$reason = 'Sqlite not available'; - } + } } } diff --git a/test/Horde/SessionHandler/Storage/Sql/SqlBaseTestCase.php b/test/Horde/SessionHandler/Storage/Sql/SqlBaseTestCase.php index cedfde2..c0d48fb 100644 --- a/test/Horde/SessionHandler/Storage/Sql/SqlBaseTestCase.php +++ b/test/Horde/SessionHandler/Storage/Sql/SqlBaseTestCase.php @@ -1,22 +1,26 @@ * @category Horde * @package Horde_SessionHandler * @subpackage UnitTests * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 + * @coversNothing */ class SqlBaseTestCase extends BaseTestCase { @@ -88,11 +92,12 @@ public static function setUpBeforeClass(): void self::$migrator = new Horde_Db_Migration_Migrator( self::$db, null,//$logger, - array('migrationsPath' => $dir, - 'schemaTableName' => 'horde_sh_schema_info')); + ['migrationsPath' => $dir, + 'schemaTableName' => 'horde_sh_schema_info'] + ); self::$migrator->up(); - self::$handler = new Horde_SessionHandler_Storage_Sql(array('db' => self::$db)); + self::$handler = new Horde_SessionHandler_Storage_Sql(['db' => self::$db]); } public static function tearDownAfterClass(): void diff --git a/test/Horde/SessionHandler/Storage/StackTest.php b/test/Horde/SessionHandler/Storage/StackTest.php index d1b8777..ee9768b 100644 --- a/test/Horde/SessionHandler/Storage/StackTest.php +++ b/test/Horde/SessionHandler/Storage/StackTest.php @@ -1,20 +1,24 @@ * @category Horde * @package Horde_SessionHandler * @subpackage UnitTests * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 + * @coversNothing */ class StackTest extends BaseTestCase { @@ -69,19 +73,19 @@ public static function setUpBeforeClass(): void { parent::setUpBeforeClass(); - $storage1 = new Horde_SessionHandler_Storage_File(array( - 'path' => self::$dir - )); - $storage2 = new Horde_SessionHandler_Storage_File(array( - 'path' => Horde_Util::createTempDir() - )); + $storage1 = new Horde_SessionHandler_Storage_File([ + 'path' => self::$dir, + ]); + $storage2 = new Horde_SessionHandler_Storage_File([ + 'path' => Horde_Util::createTempDir(), + ]); - self::$handler = new Horde_SessionHandler_Storage_Stack(array( - 'stack' => array( + self::$handler = new Horde_SessionHandler_Storage_Stack([ + 'stack' => [ $storage1, - $storage2 - ) - )); + $storage2, + ], + ]); } public function setUp(): void diff --git a/test/Horde/SessionHandler/bootstrap.php b/test/Horde/SessionHandler/bootstrap.php index bcf408d..a4f3507 100644 --- a/test/Horde/SessionHandler/bootstrap.php +++ b/test/Horde/SessionHandler/bootstrap.php @@ -1,4 +1,5 @@ Date: Wed, 8 Apr 2026 07:03:47 +0200 Subject: [PATCH 2/4] chore: metadata --- .gitignore | 26 ++++++++++++++++++++++++++ .horde.yml | 6 ++++-- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 4e78ae3..db11d2e 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,29 @@ run-tests.log /test/*/*/*/*.log /test/*/*/*/*.out + +# Added by horde-components QC --fix-qc-issues +# Build artifacts directory +/build/ +# Composer dependencies directory +/vendor/ +# Composer lock file (libraries should not commit lock files) +/composer.lock +# PHPStorm IDE settings +/.idea/ +# VSCode IDE settings +/.vscode/ +# Claude Code CLI cache and state +/.claude/ +# Cline extension data +/.cline/ +# PHP CS Fixer cache file +/.php-cs-fixer.cache +# PHPUnit result cache +/.phpunit.result.cache +# PHPUnit Cache (other) +/.phpunit.cache +# PHPStan local configuration +/phpstan.neon +# PHPStan cache directory +/.phpstan.cache/ diff --git a/.horde.yml b/.horde.yml index ebaf5f0..f2a7bf3 100644 --- a/.horde.yml +++ b/.horde.yml @@ -38,7 +38,7 @@ license: uri: http://www.horde.org/licenses/lgpl21 dependencies: required: - php: ^7.4 || ^8 + php: ^8 composer: horde/exception: ^3 horde/support: ^3 @@ -54,4 +54,6 @@ dependencies: horde/hashtable: ^2 horde/log: ^3 horde/mongo: ^2 - horde/test: ^3 +keywords: + - sessions +vendor: horde From 150c80eb52278bf1d7ea54af5e407b97d82caa91 Mon Sep 17 00:00:00 2001 From: Ralf Lang Date: Wed, 8 Apr 2026 07:30:41 +0200 Subject: [PATCH 3/4] test: Modernize test suite layout --- composer.json | 12 +- phpunit.xml.dist | 32 +- test/Horde/SessionHandler/AllTests.php | 6 - test/Horde/SessionHandler/Storage/Base.php | 93 ------ .../Horde/SessionHandler/Storage/FileTest.php | 73 ----- .../Storage/Sql/Pdo/SqliteTest.php | 37 --- test/Horde/SessionHandler/bootstrap.php | 10 - .../Storage => Unnamespaced}/BaseTestCase.php | 62 +++- .../Sql => Unnamespaced}/SqlBaseTestCase.php | 74 ++--- test/{Horde/SessionHandler => }/conf.php.dist | 0 .../Storage => integration}/MemcacheTest.php | 48 +-- .../Storage => integration}/MongoTest.php | 48 +-- .../Storage => integration}/Sql/MysqlTest.php | 19 +- .../Sql/MysqliTest.php | 19 +- .../Storage => integration}/Sql/Oci8Test.php | 21 +- .../Sql/Pdo/MysqlTest.php | 23 +- .../Sql/Pdo/PgsqlTest.php | 23 +- test/integration/Sql/Pdo/SqliteTest.php | 43 +++ .../Storage => unit}/BuiltinTest.php | 97 +++--- .../Storage => unit}/ExternalTest.php | 54 ++-- test/unit/FileTest.php | 68 ++++ test/unit/HandlerTest.php | 292 ++++++++++++++++++ .../Storage => unit}/StackTest.php | 66 ++-- test/unit/StorageConstructorTest.php | 128 ++++++++ 24 files changed, 844 insertions(+), 504 deletions(-) delete mode 100644 test/Horde/SessionHandler/AllTests.php delete mode 100644 test/Horde/SessionHandler/Storage/Base.php delete mode 100644 test/Horde/SessionHandler/Storage/FileTest.php delete mode 100644 test/Horde/SessionHandler/Storage/Sql/Pdo/SqliteTest.php delete mode 100644 test/Horde/SessionHandler/bootstrap.php rename test/{Horde/SessionHandler/Storage => Unnamespaced}/BaseTestCase.php (62%) rename test/{Horde/SessionHandler/Storage/Sql => Unnamespaced}/SqlBaseTestCase.php (62%) rename test/{Horde/SessionHandler => }/conf.php.dist (100%) rename test/{Horde/SessionHandler/Storage => integration}/MemcacheTest.php (64%) rename test/{Horde/SessionHandler/Storage => integration}/MongoTest.php (65%) rename test/{Horde/SessionHandler/Storage => integration}/Sql/MysqlTest.php (72%) rename test/{Horde/SessionHandler/Storage => integration}/Sql/MysqliTest.php (72%) rename test/{Horde/SessionHandler/Storage => integration}/Sql/Oci8Test.php (77%) rename test/{Horde/SessionHandler/Storage => integration}/Sql/Pdo/MysqlTest.php (74%) rename test/{Horde/SessionHandler/Storage => integration}/Sql/Pdo/PgsqlTest.php (74%) create mode 100644 test/integration/Sql/Pdo/SqliteTest.php rename test/{Horde/SessionHandler/Storage => unit}/BuiltinTest.php (62%) rename test/{Horde/SessionHandler/Storage => unit}/ExternalTest.php (74%) create mode 100644 test/unit/FileTest.php create mode 100644 test/unit/HandlerTest.php rename test/{Horde/SessionHandler/Storage => unit}/StackTest.php (59%) create mode 100644 test/unit/StorageConstructorTest.php diff --git a/composer.json b/composer.json index c8fa221..13c6045 100644 --- a/composer.json +++ b/composer.json @@ -32,8 +32,7 @@ "horde/db": "^3 || dev-FRAMEWORK_6_0", "horde/hashtable": "^2 || dev-FRAMEWORK_6_0", "horde/log": "^3 || dev-FRAMEWORK_6_0", - "horde/mongo": "^2 || dev-FRAMEWORK_6_0", - "horde/test": "^3 || dev-FRAMEWORK_6_0" + "horde/mongo": "^2 || dev-FRAMEWORK_6_0" }, "suggest": { "horde/db": "^3 || dev-FRAMEWORK_6_0", @@ -52,6 +51,9 @@ } }, "config": { - "allow-plugins": {} - } -} \ No newline at end of file + "allow-plugins": { + "horde/horde-installer-plugin": true + } + }, + "minimum-stability": "dev" +} diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 71b7f84..6faa1b7 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,13 +1,27 @@ - - - - lib - - + - - test + + test/unit + + + test/integration - \ No newline at end of file + + + lib + + + diff --git a/test/Horde/SessionHandler/AllTests.php b/test/Horde/SessionHandler/AllTests.php deleted file mode 100644 index 6ade172..0000000 --- a/test/Horde/SessionHandler/AllTests.php +++ /dev/null @@ -1,6 +0,0 @@ -run(); diff --git a/test/Horde/SessionHandler/Storage/Base.php b/test/Horde/SessionHandler/Storage/Base.php deleted file mode 100644 index bcb7a78..0000000 --- a/test/Horde/SessionHandler/Storage/Base.php +++ /dev/null @@ -1,93 +0,0 @@ - - * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 - * @category Horde - * @package Horde_SessionHandler - * @subpackage UnitTests - */ - -namespace Horde\SessionHandler\Storage; - -use Horde_Test_Case as TestCase; - -/** - * @coversNothing - */ -class BaseTestCase extends TestCase -{ - protected static $handler; - protected static $dir; - - protected function _write() - { - $this->assertTrue(self::$handler->open(self::$dir, 'sessionname')); - $this->assertSame('', self::$handler->read('sessionid')); - $this->assertTrue(self::$handler->write('sessionid', 'sessiondata')); - } - - protected function _read() - { - $this->assertEquals('sessiondata', self::$handler->read('sessionid')); - } - - protected function _reopen() - { - $this->assertTrue(self::$handler->close()); - $this->assertTrue(self::$handler->open(self::$dir, 'sessionname')); - $this->assertEquals('sessiondata', self::$handler->read('sessionid')); - $this->assertTrue(self::$handler->close()); - } - - protected function _list() - { - $this->assertTrue(self::$handler->close()); - $this->assertTrue(self::$handler->open(self::$dir, 'sessionname')); - self::$handler->read('sessionid2'); - $this->assertTrue(self::$handler->write('sessionid2', 'sessiondata2')); - /* List while session is active. */ - $ids = self::$handler->getSessionIDs(); - sort($ids); - $this->assertEquals(['sessionid', 'sessionid2'], $ids); - $this->assertTrue(self::$handler->close()); - - /* List while session is inactive. */ - $this->assertTrue(self::$handler->open(self::$dir, 'sessionname')); - $ids = self::$handler->getSessionIDs(); - sort($ids); - $this->assertEquals(['sessionid', 'sessionid2'], $ids); - $this->assertTrue(self::$handler->close()); - } - - protected function _destroy() - { - $this->assertTrue(self::$handler->open(self::$dir, 'sessionname')); - self::$handler->read('sessionid2'); - $this->assertTrue(self::$handler->destroy('sessionid2')); - $this->assertEquals( - ['sessionid'], - self::$handler->getSessionIDs() - ); - } - - protected function _gc() - { - $this->assertTrue(self::$handler->open(self::$dir, 'sessionname')); - $this->assertTrue(self::$handler->gc(-1)); - $this->assertEquals( - [], - self::$handler->getSessionIDs() - ); - } - - public static function setUpBeforeClass(): void - { - self::$dir = Horde_Util::createTempDir(); - } - - public static function tearDownAfterClass(): void - { - self::$handler = null; - } -} diff --git a/test/Horde/SessionHandler/Storage/FileTest.php b/test/Horde/SessionHandler/Storage/FileTest.php deleted file mode 100644 index 4521730..0000000 --- a/test/Horde/SessionHandler/Storage/FileTest.php +++ /dev/null @@ -1,73 +0,0 @@ - - * @category Horde - * @package Horde_SessionHandler - * @subpackage UnitTests - * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 - * @coversNothing - */ -class FileTest extends BaseTestCase -{ - public function testWrite() - { - $this->_write(); - } - - /** - * @depends testWrite - */ - public function testRead() - { - $this->_read(); - } - - /** - * @depends testWrite - */ - public function testReopen() - { - $this->_reopen(); - } - - /** - * @depends testWrite - */ - public function testList() - { - $this->_list(); - } - - /** - * @depends testList - */ - public function testDestroy() - { - $this->_destroy(); - } - - /** - * @depends testDestroy - */ - public function testGc() - { - $this->_gc(); - } - - public static function setUpBeforeClass(): void - { - parent::setUpBeforeClass(); - self::$handler = new Horde_SessionHandler_Storage_File(['path' => self::$dir]); - } -} diff --git a/test/Horde/SessionHandler/Storage/Sql/Pdo/SqliteTest.php b/test/Horde/SessionHandler/Storage/Sql/Pdo/SqliteTest.php deleted file mode 100644 index cd4bfa9..0000000 --- a/test/Horde/SessionHandler/Storage/Sql/Pdo/SqliteTest.php +++ /dev/null @@ -1,37 +0,0 @@ - - * @category Horde - * @package SessionHandler - * @subpackage UnitTests - * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 - * @coversNothing - */ -class SqliteTest extends SqlBaseTestCase -{ - public static function setUpBeforeClass(): void - { - $factory_db = new Horde_Test_Factory_Db(); - - try { - self::$db = $factory_db->create(); - parent::setUpBeforeClass(); - } catch (Horde_Test_Exception $e) { - self::$reason = 'Sqlite not available'; - } - } -} diff --git a/test/Horde/SessionHandler/bootstrap.php b/test/Horde/SessionHandler/bootstrap.php deleted file mode 100644 index a4f3507..0000000 --- a/test/Horde/SessionHandler/bootstrap.php +++ /dev/null @@ -1,10 +0,0 @@ - - * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 * @category Horde * @package Horde_SessionHandler * @subpackage UnitTests + * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 */ -namespace Horde\SessionHandler\Storage; +namespace Horde\SessionHandler\Test\Unnamespaced; -use Horde_Test_Case as TestCase; +use Horde_SessionHandler_Storage; use Horde_Util; +use PHPUnit\Framework\TestCase; -/** - * @coversNothing - */ -class BaseTestCase extends TestCase +abstract class BaseTestCase extends TestCase { - protected static $handler; - protected static $dir; + protected static ?Horde_SessionHandler_Storage $handler = null; + protected static string $dir = ''; - protected function _write() + protected function _write(): void { $this->assertTrue(self::$handler->open(self::$dir, 'sessionname')); $this->assertSame('', self::$handler->read('sessionid')); $this->assertTrue(self::$handler->write('sessionid', 'sessiondata')); } - protected function _read() + protected function _read(): void { $this->assertEquals('sessiondata', self::$handler->read('sessionid')); } - protected function _reopen() + protected function _reopen(): void { $this->assertTrue(self::$handler->close()); $this->assertTrue(self::$handler->open(self::$dir, 'sessionname')); @@ -41,7 +46,7 @@ protected function _reopen() $this->assertTrue(self::$handler->close()); } - protected function _list() + protected function _list(): void { $this->assertTrue(self::$handler->close()); $this->assertTrue(self::$handler->open(self::$dir, 'sessionname')); @@ -61,7 +66,7 @@ protected function _list() $this->assertTrue(self::$handler->close()); } - protected function _destroy() + protected function _destroy(): void { $this->assertTrue(self::$handler->open(self::$dir, 'sessionname')); self::$handler->read('sessionid2'); @@ -72,7 +77,7 @@ protected function _destroy() ); } - protected function _gc() + protected function _gc(): void { $this->assertTrue(self::$handler->open(self::$dir, 'sessionname')); $this->assertTrue(self::$handler->gc(-1)); @@ -82,6 +87,33 @@ protected function _gc() ); } + /** + * Load test configuration from environment variable or conf.php file. + * + * Replaces Horde_Test_Case::getConfig() to avoid horde/test dependency. + */ + public static function getConfig(string $env, ?string $path = null): ?array + { + $config = getenv($env); + if ($config) { + $json = json_decode($config, true); + if ($json) { + return $json; + } + } + + if ($path) { + $configFile = $path . '/conf.php'; + if (file_exists($configFile)) { + $conf = []; + require $configFile; + return $conf; + } + } + + return null; + } + public static function setUpBeforeClass(): void { self::$dir = Horde_Util::createTempDir(); diff --git a/test/Horde/SessionHandler/Storage/Sql/SqlBaseTestCase.php b/test/Unnamespaced/SqlBaseTestCase.php similarity index 62% rename from test/Horde/SessionHandler/Storage/Sql/SqlBaseTestCase.php rename to test/Unnamespaced/SqlBaseTestCase.php index c0d48fb..6a23149 100644 --- a/test/Horde/SessionHandler/Storage/Sql/SqlBaseTestCase.php +++ b/test/Unnamespaced/SqlBaseTestCase.php @@ -1,76 +1,68 @@ * @category Horde * @package Horde_SessionHandler * @subpackage UnitTests * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 - * @coversNothing */ -class SqlBaseTestCase extends BaseTestCase -{ - protected static $db; - protected static $migrator; +namespace Horde\SessionHandler\Test\Unnamespaced; + +use Horde_Db_Adapter; +use Horde_Db_Migration_Migrator; +use Horde_Log_Handler_Cli; +use Horde_Log_Logger; +use Horde_SessionHandler_Storage_Sql; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\Depends; - protected static $reason; +#[CoversClass(Horde_SessionHandler_Storage_Sql::class)] +abstract class SqlBaseTestCase extends BaseTestCase +{ + protected static ?Horde_Db_Adapter $db = null; + protected static ?Horde_Db_Migration_Migrator $migrator = null; + protected static string $reason = ''; - public function testWrite() + public function testWrite(): void { $this->_write(); } - /** - * @depends testWrite - */ - public function testRead() + #[Depends('testWrite')] + public function testRead(): void { $this->_read(); } - /** - * @depends testWrite - */ - public function testReopen() + #[Depends('testWrite')] + public function testReopen(): void { $this->_reopen(); } - /** - * @depends testWrite - */ - public function testList() + #[Depends('testWrite')] + public function testList(): void { $this->_list(); } - /** - * @depends testList - */ - public function testDestroy() + #[Depends('testList')] + public function testDestroy(): void { $this->_destroy(); } - /** - * @depends testDestroy - */ - public function testGc() + #[Depends('testDestroy')] + public function testGc(): void { $this->_gc(); } @@ -79,9 +71,7 @@ public static function setUpBeforeClass(): void { parent::setUpBeforeClass(); - $logger = new Horde_Log_Logger(new Horde_Log_Handler_Cli()); - //self::$db->setLogger($logger); - $dir = dirname(__FILE__) . '/../../../../../migration/Horde/SessionHandler'; + $dir = dirname(__FILE__) . '/../../migration/Horde/SessionHandler'; if (!is_dir($dir)) { error_reporting(E_ALL & ~E_DEPRECATED); $dir = PEAR_Config::singleton() @@ -91,7 +81,7 @@ public static function setUpBeforeClass(): void } self::$migrator = new Horde_Db_Migration_Migrator( self::$db, - null,//$logger, + null, ['migrationsPath' => $dir, 'schemaTableName' => 'horde_sh_schema_info'] ); diff --git a/test/Horde/SessionHandler/conf.php.dist b/test/conf.php.dist similarity index 100% rename from test/Horde/SessionHandler/conf.php.dist rename to test/conf.php.dist diff --git a/test/Horde/SessionHandler/Storage/MemcacheTest.php b/test/integration/MemcacheTest.php similarity index 64% rename from test/Horde/SessionHandler/Storage/MemcacheTest.php rename to test/integration/MemcacheTest.php index 13d39e9..ff5607a 100644 --- a/test/Horde/SessionHandler/Storage/MemcacheTest.php +++ b/test/integration/MemcacheTest.php @@ -1,58 +1,58 @@ * @category Horde * @package Horde_SessionHandler * @subpackage UnitTests * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 - * @coversNothing */ + +namespace Horde\SessionHandler\Test\Integration; + +use Horde\SessionHandler\Test\Unnamespaced\BaseTestCase; +use Horde_SessionHandler_Storage_Memcache; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\Depends; +use Horde_Memcache; + +#[CoversClass(Horde_SessionHandler_Storage_Memcache::class)] class MemcacheTest extends BaseTestCase { - protected static $reason; + protected static string $reason = ''; - public function testWrite() + public function testWrite(): void { $this->_write(); } - /** - * @depends testWrite - */ - public function testRead() + #[Depends('testWrite')] + public function testRead(): void { $this->_read(); } - /** - * @depends testWrite - */ - public function testReopen() + #[Depends('testWrite')] + public function testReopen(): void { $this->_reopen(); } - /** - * @depends testWrite - */ - public function testList() + #[Depends('testWrite')] + public function testList(): void { $this->_list(); } - /** - * @depends testList - */ - public function testDestroy() + #[Depends('testList')] + public function testDestroy(): void { $this->_destroy(); } diff --git a/test/Horde/SessionHandler/Storage/MongoTest.php b/test/integration/MongoTest.php similarity index 65% rename from test/Horde/SessionHandler/Storage/MongoTest.php rename to test/integration/MongoTest.php index 78d02a0..09263dc 100644 --- a/test/Horde/SessionHandler/Storage/MongoTest.php +++ b/test/integration/MongoTest.php @@ -1,59 +1,59 @@ * @category Horde * @package Horde_SessionHandler * @subpackage UnitTests * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 - * @coversNothing */ + +namespace Horde\SessionHandler\Test\Integration; + +use Horde\SessionHandler\Test\Unnamespaced\BaseTestCase; +use Horde_SessionHandler_Storage_Mongo; +use Horde_Test_Factory_Mongo; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\Depends; + +#[CoversClass(Horde_SessionHandler_Storage_Mongo::class)] class MongoTest extends BaseTestCase { - protected static $reason; + protected static string $reason = ''; protected static $mongo; - public function testWrite() + public function testWrite(): void { $this->_write(); } - /** - * @depends testWrite - */ - public function testRead() + #[Depends('testWrite')] + public function testRead(): void { $this->_read(); } - /** - * @depends testWrite - */ - public function testReopen() + #[Depends('testWrite')] + public function testReopen(): void { $this->_reopen(); } - /** - * @depends testWrite - */ - public function testList() + #[Depends('testWrite')] + public function testList(): void { $this->_list(); } - /** - * @depends testList - */ - public function testDestroy() + #[Depends('testList')] + public function testDestroy(): void { $this->_destroy(); } diff --git a/test/Horde/SessionHandler/Storage/Sql/MysqlTest.php b/test/integration/Sql/MysqlTest.php similarity index 72% rename from test/Horde/SessionHandler/Storage/Sql/MysqlTest.php rename to test/integration/Sql/MysqlTest.php index 4fd250a..6b42110 100644 --- a/test/Horde/SessionHandler/Storage/Sql/MysqlTest.php +++ b/test/integration/Sql/MysqlTest.php @@ -1,19 +1,26 @@ * @category Horde - * @package SessionHandler + * @package Horde_SessionHandler * @subpackage UnitTests * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 + */ + +namespace Horde\SessionHandler\Test\Integration\Sql; + +use Horde\SessionHandler\Test\Unnamespaced\SqlBaseTestCase; +use Horde_Db_Adapter_Mysql; + +/** * @coversNothing */ class MysqlTest extends SqlBaseTestCase diff --git a/test/Horde/SessionHandler/Storage/Sql/MysqliTest.php b/test/integration/Sql/MysqliTest.php similarity index 72% rename from test/Horde/SessionHandler/Storage/Sql/MysqliTest.php rename to test/integration/Sql/MysqliTest.php index 254dae1..f85ab5e 100644 --- a/test/Horde/SessionHandler/Storage/Sql/MysqliTest.php +++ b/test/integration/Sql/MysqliTest.php @@ -1,19 +1,26 @@ * @category Horde - * @package SessionHandler + * @package Horde_SessionHandler * @subpackage UnitTests * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 + */ + +namespace Horde\SessionHandler\Test\Integration\Sql; + +use Horde\SessionHandler\Test\Unnamespaced\SqlBaseTestCase; +use Horde_Db_Adapter_Mysqli; + +/** * @coversNothing */ class MysqliTest extends SqlBaseTestCase diff --git a/test/Horde/SessionHandler/Storage/Sql/Oci8Test.php b/test/integration/Sql/Oci8Test.php similarity index 77% rename from test/Horde/SessionHandler/Storage/Sql/Oci8Test.php rename to test/integration/Sql/Oci8Test.php index 9ec74a5..61fb2f7 100644 --- a/test/Horde/SessionHandler/Storage/Sql/Oci8Test.php +++ b/test/integration/Sql/Oci8Test.php @@ -1,19 +1,26 @@ * @category Horde - * @package SessionHandler + * @package Horde_SessionHandler * @subpackage UnitTests * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 + */ + +namespace Horde\SessionHandler\Test\Integration\Sql; + +use Horde\SessionHandler\Test\Unnamespaced\SqlBaseTestCase; +use Horde_Db_Adapter_Oci8; + +/** * @coversNothing */ class Oci8Test extends SqlBaseTestCase @@ -36,7 +43,7 @@ public static function setUpBeforeClass(): void parent::setUpBeforeClass(); } - public function testLargeWrite() + public function testLargeWrite(): void { $this->assertTrue(self::$handler->open(self::$dir, 'sessiondata')); $this->assertSame('', self::$handler->read('largedata')); diff --git a/test/Horde/SessionHandler/Storage/Sql/Pdo/MysqlTest.php b/test/integration/Sql/Pdo/MysqlTest.php similarity index 74% rename from test/Horde/SessionHandler/Storage/Sql/Pdo/MysqlTest.php rename to test/integration/Sql/Pdo/MysqlTest.php index f9f6eb7..32262a2 100644 --- a/test/Horde/SessionHandler/Storage/Sql/Pdo/MysqlTest.php +++ b/test/integration/Sql/Pdo/MysqlTest.php @@ -1,22 +1,27 @@ * @category Horde - * @package SessionHandler + * @package Horde_SessionHandler * @subpackage UnitTests * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 + */ + +namespace Horde\SessionHandler\Test\Integration\Sql\Pdo; + +use Horde\SessionHandler\Test\Unnamespaced\SqlBaseTestCase; +use Horde_Db_Adapter_Pdo_Mysql; +use PDO; + +/** * @coversNothing */ class MysqlTest extends SqlBaseTestCase diff --git a/test/Horde/SessionHandler/Storage/Sql/Pdo/PgsqlTest.php b/test/integration/Sql/Pdo/PgsqlTest.php similarity index 74% rename from test/Horde/SessionHandler/Storage/Sql/Pdo/PgsqlTest.php rename to test/integration/Sql/Pdo/PgsqlTest.php index 4512615..912ce6b 100644 --- a/test/Horde/SessionHandler/Storage/Sql/Pdo/PgsqlTest.php +++ b/test/integration/Sql/Pdo/PgsqlTest.php @@ -1,22 +1,27 @@ * @category Horde - * @package SessionHandler + * @package Horde_SessionHandler * @subpackage UnitTests * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 + */ + +namespace Horde\SessionHandler\Test\Integration\Sql\Pdo; + +use Horde\SessionHandler\Test\Unnamespaced\SqlBaseTestCase; +use Horde_Db_Adapter_Pdo_Pgsql; +use PDO; + +/** * @coversNothing */ class PgsqlTest extends SqlBaseTestCase diff --git a/test/integration/Sql/Pdo/SqliteTest.php b/test/integration/Sql/Pdo/SqliteTest.php new file mode 100644 index 0000000..ea330a3 --- /dev/null +++ b/test/integration/Sql/Pdo/SqliteTest.php @@ -0,0 +1,43 @@ + + * @category Horde + * @package Horde_SessionHandler + * @subpackage UnitTests + * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 + */ + +namespace Horde\SessionHandler\Test\Integration\Sql\Pdo; + +use Horde\SessionHandler\Test\Unnamespaced\SqlBaseTestCase; +use Horde_Db_Adapter_Pdo_Sqlite; +use Exception; + +/** + * @coversNothing + */ +class SqliteTest extends SqlBaseTestCase +{ + public static function setUpBeforeClass(): void + { + if (!extension_loaded('pdo_sqlite')) { + self::$reason = 'PDO SQLite extension not available'; + return; + } + + try { + self::$db = new Horde_Db_Adapter_Pdo_Sqlite(['dbname' => ':memory:']); + parent::setUpBeforeClass(); + } catch (Exception $e) { + self::$reason = 'SQLite not available: ' . $e->getMessage(); + } + } +} diff --git a/test/Horde/SessionHandler/Storage/BuiltinTest.php b/test/unit/BuiltinTest.php similarity index 62% rename from test/Horde/SessionHandler/Storage/BuiltinTest.php rename to test/unit/BuiltinTest.php index 6c2fce6..189bf75 100644 --- a/test/Horde/SessionHandler/Storage/BuiltinTest.php +++ b/test/unit/BuiltinTest.php @@ -1,46 +1,49 @@ * @category Horde * @package Horde_SessionHandler * @subpackage UnitTests * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 - * @coversNothing */ -class BuiltinTest extends BaseTestCase + +namespace Horde\SessionHandler\Test\Unit; + +use Horde_SessionHandler_Storage_Builtin; +use Horde_Util; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\RunInSeparateProcess; +use PHPUnit\Framework\TestCase; + +#[CoversClass(Horde_SessionHandler_Storage_Builtin::class)] +class BuiltinTest extends TestCase { - /** - * @runInSeparateProcess - */ - public function testWrite() + protected static Horde_SessionHandler_Storage_Builtin $handler; + protected static string $dir; + + #[RunInSeparateProcess] + public function testWrite(): void { $this->_write(); } - /** - * @runInSeparateProcess - */ - public function testRead() + #[RunInSeparateProcess] + public function testRead(): void { $this->_write(); $this->assertEquals('sessiondata|s:3:"foo";', self::$handler->read('sessionid')); } - /** - * @runInSeparateProcess - */ - public function testReopen() + #[RunInSeparateProcess] + public function testReopen(): void { $this->_write(); session_write_close(); @@ -51,10 +54,8 @@ public function testReopen() session_write_close(); } - /** - * @runInSeparateProcess - */ - public function testList() + #[RunInSeparateProcess] + public function testList(): void { $this->_write(); session_write_close(); @@ -74,10 +75,8 @@ public function testList() $this->assertEquals(['sessionid', 'sessionid2'], $ids); } - /** - * @runInSeparateProcess - */ - public function testDestroy() + #[RunInSeparateProcess] + public function testDestroy(): void { $this->testList(); session_name('sessionname'); @@ -96,18 +95,13 @@ public function testDestroy() ); } - /** - * @runInSeparateProcess - */ - public function testGc() + #[RunInSeparateProcess] + public function testGc(): void { $this->testDestroy(); - $this->probability = ini_get('session.gc_probability'); - $this->divisor = ini_get('session.gc_divisor'); - $this->maxlifetime = ini_get('session.gc_maxlifetime'); - ini_set('session.gc_probability', 100); - ini_set('session.gc_divisor', 1); - ini_set('session.gc_maxlifetime', -1); + ini_set('session.gc_probability', '100'); + ini_set('session.gc_divisor', '1'); + ini_set('session.gc_maxlifetime', '-1'); session_name('sessionname'); session_start(); $this->assertEquals( @@ -116,7 +110,7 @@ public function testGc() ); } - protected function _write() + protected function _write(): void { session_name('sessionname'); session_id('sessionid'); @@ -128,40 +122,23 @@ protected function _write() public static function setUpBeforeClass(): void { - parent::setUpBeforeClass(); + self::$dir = Horde_Util::createTempDir(); if (!headers_sent()) { session_cache_limiter(''); - ini_set('session.use_cookies', 0); + ini_set('session.use_cookies', '0'); ini_set('session.save_path', self::$dir); } self::$handler = new Horde_SessionHandler_Storage_Builtin(['path' => self::$dir]); } - public function tearDown(): void - { - if (isset($this->probability)) { - ini_set('session.gc_probability', $this->probability); - ini_set('session.gc_divisor', $this->divisor); - ini_set('session.gc_maxlifetime', $this->maxlifetime); - } - } - - /** - * @todo Rely on session_status() in H6. - */ public static function tearDownAfterClass(): void { - parent::tearDownAfterClass(); unset($_SESSION); - if ((function_exists('session_status') - && session_status() == PHP_SESSION_ACTIVE) - || (!function_exists('session_status') - && session_id())) { + if (session_status() == PHP_SESSION_ACTIVE) { session_destroy(); } if (!headers_sent()) { session_name(ini_get('session.name')); } } - } diff --git a/test/Horde/SessionHandler/Storage/ExternalTest.php b/test/unit/ExternalTest.php similarity index 74% rename from test/Horde/SessionHandler/Storage/ExternalTest.php rename to test/unit/ExternalTest.php index 56d5c18..4104b5d 100644 --- a/test/Horde/SessionHandler/Storage/ExternalTest.php +++ b/test/unit/ExternalTest.php @@ -1,43 +1,44 @@ * @category Horde * @package Horde_SessionHandler * @subpackage UnitTests * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 - * @coversNothing */ + +namespace Horde\SessionHandler\Test\Unit; + +use Horde\SessionHandler\Test\Unnamespaced\BaseTestCase; +use Horde_SessionHandler_Storage_External; +use Horde_SessionHandler_Storage_File; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\Depends; + +#[CoversClass(Horde_SessionHandler_Storage_External::class)] class ExternalTest extends BaseTestCase { - public function testWrite() + public function testWrite(): void { $this->_write(); } - /** - * @depends testWrite - */ - public function testRead() + #[Depends('testWrite')] + public function testRead(): void { $this->_read(); } - /** - * @depends testWrite - */ - public function testReopen() + #[Depends('testWrite')] + public function testReopen(): void { $this->_reopen(); } @@ -45,10 +46,9 @@ public function testReopen() /** * The external driver doesn't support listing, so test for existing * sessions manually. - * - * @depends testWrite */ - public function testList() + #[Depends('testWrite')] + public function testList(): void { self::$handler->close(); self::$handler->open(self::$dir, 'sessionname'); @@ -66,10 +66,8 @@ public function testList() self::$handler->close(); } - /** - * @depends testList - */ - public function testDestroy() + #[Depends('testList')] + public function testDestroy(): void { self::$handler->open(self::$dir, 'sessionname'); self::$handler->read('sessionid2'); @@ -77,10 +75,8 @@ public function testDestroy() $this->assertSame('', self::$handler->read('sessionid2')); } - /** - * @depends testDestroy - */ - public function testGc() + #[Depends('testDestroy')] + public function testGc(): void { self::$handler->open(self::$dir, 'sessionname'); self::$handler->gc(-1); diff --git a/test/unit/FileTest.php b/test/unit/FileTest.php new file mode 100644 index 0000000..4351bda --- /dev/null +++ b/test/unit/FileTest.php @@ -0,0 +1,68 @@ + + * @category Horde + * @package Horde_SessionHandler + * @subpackage UnitTests + * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 + */ + +namespace Horde\SessionHandler\Test\Unit; + +use Horde\SessionHandler\Test\Unnamespaced\BaseTestCase; +use Horde_SessionHandler_Storage_File; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\Depends; + +#[CoversClass(Horde_SessionHandler_Storage_File::class)] +class FileTest extends BaseTestCase +{ + public function testWrite(): void + { + $this->_write(); + } + + #[Depends('testWrite')] + public function testRead(): void + { + $this->_read(); + } + + #[Depends('testWrite')] + public function testReopen(): void + { + $this->_reopen(); + } + + #[Depends('testWrite')] + public function testList(): void + { + $this->_list(); + } + + #[Depends('testList')] + public function testDestroy(): void + { + $this->_destroy(); + } + + #[Depends('testDestroy')] + public function testGc(): void + { + $this->_gc(); + } + + public static function setUpBeforeClass(): void + { + parent::setUpBeforeClass(); + self::$handler = new Horde_SessionHandler_Storage_File(['path' => self::$dir]); + } +} diff --git a/test/unit/HandlerTest.php b/test/unit/HandlerTest.php new file mode 100644 index 0000000..cb2c697 --- /dev/null +++ b/test/unit/HandlerTest.php @@ -0,0 +1,292 @@ + self::$dir]); + return new Horde_SessionHandler($storage, array_merge(['noset' => true], $params)); + } + + public function testOpenDelegatesToStorage(): void + { + $handler = $this->createHandler(); + $this->assertTrue($handler->open(self::$dir, 'test')); + } + + public function testOpenOnlyConnectsOnce(): void + { + $storage = $this->createMock(Horde_SessionHandler_Storage::class); + $storage->expects($this->once())->method('open'); + $handler = new Horde_SessionHandler($storage, ['noset' => true]); + $handler->open(self::$dir, 'test'); + $handler->open(self::$dir, 'test'); + } + + public function testOpenReturnsFalseOnException(): void + { + $storage = $this->createMock(Horde_SessionHandler_Storage::class); + $storage->expects($this->once()) + ->method('open') + ->willThrowException(new Horde_SessionHandler_Exception('fail')); + $handler = new Horde_SessionHandler($storage, ['noset' => true]); + $this->assertFalse($handler->open(self::$dir, 'test')); + } + + public function testCloseResetsConnection(): void + { + $storage = $this->createMock(Horde_SessionHandler_Storage::class); + $storage->expects($this->exactly(2))->method('open'); + $storage->expects($this->once())->method('close'); + $handler = new Horde_SessionHandler($storage, ['noset' => true]); + + $handler->open(self::$dir, 'test'); + $handler->close(); + // After close, open should call storage->open again + $handler->open(self::$dir, 'test'); + } + + public function testCloseReturnsTrueEvenOnException(): void + { + $storage = $this->createMock(Horde_SessionHandler_Storage::class); + $storage->expects($this->once()) + ->method('close') + ->willThrowException(new Horde_SessionHandler_Exception('fail')); + $handler = new Horde_SessionHandler($storage, ['noset' => true]); + $this->assertTrue($handler->close()); + } + + public function testReadReturnsDataFromStorage(): void + { + $handler = $this->createHandler(); + $handler->open(self::$dir, 'test'); + $handler->write('read-test', 'hello'); + // Close and reopen to flush write + $handler->close(); + $handler->open(self::$dir, 'test'); + // Need a fresh handler to avoid md5 signature caching + $handler2 = $this->createHandler(); + $handler2->open(self::$dir, 'test'); + $this->assertEquals('hello', $handler2->read('read-test')); + } + + public function testWriteSkipsWhenDataUnchanged(): void + { + $storage = $this->createMock(Horde_SessionHandler_Storage::class); + $storage->method('read')->willReturn('same-data'); + // write should NOT be called because md5 signature matches + $storage->expects($this->never())->method('write'); + $handler = new Horde_SessionHandler($storage, ['noset' => true]); + $handler->read('test-id'); + $handler->write('test-id', 'same-data'); + } + + public function testWriteProceedsWhenDataChanged(): void + { + $storage = $this->createMock(Horde_SessionHandler_Storage::class); + $storage->method('read')->willReturn('old-data'); + $storage->expects($this->once()) + ->method('write') + ->with('test-id', 'new-data') + ->willReturn(true); + $handler = new Horde_SessionHandler($storage, ['noset' => true]); + $handler->read('test-id'); + $this->assertTrue($handler->write('test-id', 'new-data')); + } + + public function testWriteProceedsWhenChangedFlagSet(): void + { + $storage = $this->createMock(Horde_SessionHandler_Storage::class); + $storage->method('read')->willReturn('same-data'); + $storage->expects($this->once()) + ->method('write') + ->with('test-id', 'same-data') + ->willReturn(true); + $handler = new Horde_SessionHandler($storage, ['noset' => true]); + $handler->read('test-id'); + $handler->changed = true; + $this->assertTrue($handler->write('test-id', 'same-data')); + } + + public function testWriteReturnsFalseOnStorageFailure(): void + { + $storage = $this->createMock(Horde_SessionHandler_Storage::class); + $storage->method('read')->willReturn('old-data'); + $storage->expects($this->once()) + ->method('write') + ->willReturn(false); + $handler = new Horde_SessionHandler($storage, ['noset' => true]); + $handler->read('test-id'); + $this->assertFalse($handler->write('test-id', 'new-data')); + } + + public function testWriteSkipsMd5WhenNoMd5ParamSet(): void + { + $storage = $this->createMock(Horde_SessionHandler_Storage::class); + $storage->method('read')->willReturn('same-data'); + // With no_md5, write should be skipped unless changed flag is set + $storage->expects($this->never())->method('write'); + $handler = new Horde_SessionHandler($storage, ['noset' => true, 'no_md5' => true]); + $handler->read('test-id'); + $handler->write('test-id', 'same-data'); + } + + public function testWriteWithNoMd5AndChangedFlag(): void + { + $storage = $this->createMock(Horde_SessionHandler_Storage::class); + $storage->method('read')->willReturn('same-data'); + $storage->expects($this->once()) + ->method('write') + ->willReturn(true); + $handler = new Horde_SessionHandler($storage, ['noset' => true, 'no_md5' => true]); + $handler->read('test-id'); + $handler->changed = true; + $this->assertTrue($handler->write('test-id', 'same-data')); + } + + public function testDestroyReturnsTrueOnSuccess(): void + { + $storage = $this->createMock(Horde_SessionHandler_Storage::class); + $storage->expects($this->once()) + ->method('destroy') + ->with('session-123') + ->willReturn(true); + $handler = new Horde_SessionHandler($storage, ['noset' => true]); + $this->assertTrue($handler->destroy('session-123')); + } + + public function testDestroyReturnsFalseOnFailure(): void + { + $storage = $this->createMock(Horde_SessionHandler_Storage::class); + $storage->expects($this->once()) + ->method('destroy') + ->willReturn(false); + $handler = new Horde_SessionHandler($storage, ['noset' => true]); + $this->assertFalse($handler->destroy('session-123')); + } + + public function testGcDelegatesToStorage(): void + { + $storage = $this->createMock(Horde_SessionHandler_Storage::class); + $storage->expects($this->once()) + ->method('gc') + ->with(600) + ->willReturn(true); + $handler = new Horde_SessionHandler($storage, ['noset' => true]); + $handler->gc(600); + } + + public function testGetSessionIDsDelegatesToStorage(): void + { + $storage = $this->createMock(Horde_SessionHandler_Storage::class); + $storage->expects($this->once()) + ->method('getSessionIDs') + ->willReturn(['id1', 'id2']); + $handler = new Horde_SessionHandler($storage, ['noset' => true]); + $this->assertEquals(['id1', 'id2'], $handler->getSessionIDs()); + } + + public function testGetSessionsInfoReturnsEmptyWithoutParser(): void + { + $handler = $this->createHandler(); + $this->assertEquals([], $handler->getSessionsInfo()); + } + + public function testGetSessionsInfoReturnsEmptyWithNonCallableParser(): void + { + $handler = $this->createHandler(params: ['parse' => 'not_a_function_xyz']); + $this->assertEquals([], $handler->getSessionsInfo()); + } + + public function testGetSessionsInfoParsesSessionData(): void + { + $storage = new Horde_SessionHandler_Storage_File(['path' => self::$dir]); + $handler = new Horde_SessionHandler($storage, [ + 'noset' => true, + 'parse' => function (string $data) { + if ($data === '') { + return false; + } + return ['raw' => $data]; + }, + ]); + + // Write some session data + $storage->open(self::$dir, 'test'); + $storage->write('info-test-1', 'user=alice'); + $storage->close(); + + $info = $handler->getSessionsInfo(); + $this->assertArrayHasKey('info-test-1', $info); + $this->assertEquals(['raw' => 'user=alice'], $info['info-test-1']); + } + + public function testGetSessionsInfoSkipsUnparsableSessions(): void + { + $dir = Horde_Util::createTempDir(); + $storage = new Horde_SessionHandler_Storage_File(['path' => $dir]); + $handler = new Horde_SessionHandler($storage, [ + 'noset' => true, + 'parse' => function (string $data) { + if ($data === 'skip-me') { + return false; + } + return ['data' => $data]; + }, + ]); + + $storage->open($dir, 'test'); + $storage->write('parseable', 'good-data'); + $storage->close(); + $storage->open($dir, 'test'); + $storage->write('unparseable', 'skip-me'); + $storage->close(); + + $info = $handler->getSessionsInfo(); + $this->assertArrayHasKey('parseable', $info); + $this->assertArrayNotHasKey('unparseable', $info); + } + + public function testPublicProperties(): void + { + $handler = $this->createHandler(); + $this->assertSame('', $handler->data); + $this->assertSame('', $handler->id); + $this->assertSame('', $handler->name); + $this->assertFalse($handler->changed); + } +} diff --git a/test/Horde/SessionHandler/Storage/StackTest.php b/test/unit/StackTest.php similarity index 59% rename from test/Horde/SessionHandler/Storage/StackTest.php rename to test/unit/StackTest.php index ee9768b..b523a0e 100644 --- a/test/Horde/SessionHandler/Storage/StackTest.php +++ b/test/unit/StackTest.php @@ -1,70 +1,63 @@ * @category Horde * @package Horde_SessionHandler * @subpackage UnitTests * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 - * @coversNothing */ + +namespace Horde\SessionHandler\Test\Unit; + +use Horde\SessionHandler\Test\Unnamespaced\BaseTestCase; +use Horde_SessionHandler_Storage_File; +use Horde_SessionHandler_Storage_Stack; +use Horde_Util; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\Depends; + +#[CoversClass(Horde_SessionHandler_Storage_Stack::class)] class StackTest extends BaseTestCase { - public static $reason; - - public function testWrite() + public function testWrite(): void { $this->_write(); } - /** - * @depends testWrite - */ - public function testRead() + #[Depends('testWrite')] + public function testRead(): void { $this->_read(); } - /** - * @depends testWrite - */ - public function testReopen() + #[Depends('testWrite')] + public function testReopen(): void { $this->_reopen(); } - /** - * @depends testWrite - */ - public function testList() + #[Depends('testWrite')] + public function testList(): void { $this->_list(); } - /** - * @depends testList - */ - public function testDestroy() + #[Depends('testList')] + public function testDestroy(): void { $this->_destroy(); } - /** - * @depends testDestroy - */ - public function testGc() + #[Depends('testDestroy')] + public function testGc(): void { $this->_gc(); } @@ -87,11 +80,4 @@ public static function setUpBeforeClass(): void ], ]); } - - public function setUp(): void - { - if (!self::$handler) { - $this->markTestSkipped(self::$reason); - } - } } diff --git a/test/unit/StorageConstructorTest.php b/test/unit/StorageConstructorTest.php new file mode 100644 index 0000000..25416a3 --- /dev/null +++ b/test/unit/StorageConstructorTest.php @@ -0,0 +1,128 @@ +expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Missing path parameter'); + new Horde_SessionHandler_Storage_File(); + } + + public function testFileStorageTrimsTrailingSlash(): void + { + $storage = new Horde_SessionHandler_Storage_File(['path' => '/tmp/test/']); + // The storage was created successfully - path was accepted and trimmed + $this->assertInstanceOf(Horde_SessionHandler_Storage_File::class, $storage); + } + + public function testExternalStorageRequiresAllCallbacks(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Missing parameter: open'); + new Horde_SessionHandler_Storage_External([ + 'close' => fn() => true, + 'read' => fn() => '', + 'write' => fn() => true, + 'destroy' => fn() => true, + 'gc' => fn() => true, + ]); + } + + #[DataProvider('missingExternalCallbackProvider')] + public function testExternalStorageRejectsMissingCallback(string $missing): void + { + $callbacks = [ + 'open' => fn() => true, + 'close' => fn() => true, + 'read' => fn() => '', + 'write' => fn() => true, + 'destroy' => fn() => true, + 'gc' => fn() => true, + ]; + unset($callbacks[$missing]); + + $this->expectException(InvalidArgumentException::class); + new Horde_SessionHandler_Storage_External($callbacks); + } + + public static function missingExternalCallbackProvider(): array + { + return [ + 'missing open' => ['open'], + 'missing close' => ['close'], + 'missing read' => ['read'], + 'missing write' => ['write'], + 'missing destroy' => ['destroy'], + 'missing gc' => ['gc'], + ]; + } + + public function testStackStorageRequiresStack(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Missing stack parameter'); + new Horde_SessionHandler_Storage_Stack(); + } + + public function testStorageSleepThrowsLogicException(): void + { + $storage = new Horde_SessionHandler_Storage_File(['path' => '/tmp']); + $this->expectException(LogicException::class); + $this->expectExceptionMessage('do not support serialization'); + $storage->__sleep(); + } + + public function testStorageReadonlyProperty(): void + { + $storage = new Horde_SessionHandler_Storage_File(['path' => '/tmp']); + $this->assertFalse($storage->readonly); + $storage->readonly = true; + $this->assertTrue($storage->readonly); + } + + public function testExternalGetSessionIDsThrowsException(): void + { + $storage = new Horde_SessionHandler_Storage_External([ + 'open' => fn() => true, + 'close' => fn() => true, + 'read' => fn() => '', + 'write' => fn() => true, + 'destroy' => fn() => true, + 'gc' => fn() => true, + ]); + $this->expectException(Horde_SessionHandler_Exception::class); + $storage->getSessionIDs(); + } +} From ab1893a21eaadaf9609a9753101257e03fb2846a Mon Sep 17 00:00:00 2001 From: Ralf Lang Date: Wed, 8 Apr 2026 08:20:28 +0200 Subject: [PATCH 4/4] test: Add unit tests (with mocks) mimicking the integration tests (real IO) --- test/unit/ExternalUnitTest.php | 147 +++++++++++++ test/unit/HandlerTest.php | 12 +- test/unit/HashtableUnitTest.php | 319 ++++++++++++++++++++++++++++ test/unit/SqlUnitTest.php | 359 ++++++++++++++++++++++++++++++++ test/unit/StackUnitTest.php | 198 ++++++++++++++++++ 5 files changed, 1029 insertions(+), 6 deletions(-) create mode 100644 test/unit/ExternalUnitTest.php create mode 100644 test/unit/HashtableUnitTest.php create mode 100644 test/unit/SqlUnitTest.php create mode 100644 test/unit/StackUnitTest.php diff --git a/test/unit/ExternalUnitTest.php b/test/unit/ExternalUnitTest.php new file mode 100644 index 0000000..938145b --- /dev/null +++ b/test/unit/ExternalUnitTest.php @@ -0,0 +1,147 @@ + fn() => true, + 'close' => fn() => true, + 'read' => fn() => '', + 'write' => fn() => true, + 'destroy' => fn() => true, + 'gc' => fn() => true, + ]; + return new Horde_SessionHandler_Storage_External(array_merge($defaults, $overrides)); + } + + public function testOpenDelegatesToCallback(): void + { + $called = false; + $storage = $this->createExternal([ + 'open' => function ($path, $name) use (&$called) { + $called = true; + $this->assertEquals('/tmp', $path); + $this->assertEquals('sess', $name); + return true; + }, + ]); + $this->assertTrue($storage->open('/tmp', 'sess')); + $this->assertTrue($called); + } + + public function testCloseDelegatesToCallback(): void + { + $called = false; + $storage = $this->createExternal([ + 'close' => function () use (&$called) { + $called = true; + return true; + }, + ]); + $storage->close(); + $this->assertTrue($called); + } + + public function testReadDelegatesToCallback(): void + { + $storage = $this->createExternal([ + 'read' => fn($id) => 'data-for-' . $id, + ]); + $this->assertEquals('data-for-abc', $storage->read('abc')); + } + + public function testWriteDelegatesToCallback(): void + { + $receivedArgs = []; + $storage = $this->createExternal([ + 'write' => function ($id, $data) use (&$receivedArgs) { + $receivedArgs = [$id, $data]; + return true; + }, + ]); + $this->assertTrue($storage->write('sid', 'session-data')); + $this->assertEquals(['sid', 'session-data'], $receivedArgs); + } + + public function testDestroyDelegatesToCallback(): void + { + $destroyedId = null; + $storage = $this->createExternal([ + 'destroy' => function ($id) use (&$destroyedId) { + $destroyedId = $id; + return true; + }, + ]); + $this->assertTrue($storage->destroy('del-me')); + $this->assertEquals('del-me', $destroyedId); + } + + public function testGcDelegatesToCallback(): void + { + $receivedLifetime = null; + $storage = $this->createExternal([ + 'gc' => function ($maxlifetime) use (&$receivedLifetime) { + $receivedLifetime = $maxlifetime; + return true; + }, + ]); + $storage->gc(600); + $this->assertEquals(600, $receivedLifetime); + } + + public function testGcUsesDefaultMaxlifetime(): void + { + $receivedLifetime = null; + $storage = $this->createExternal([ + 'gc' => function ($maxlifetime) use (&$receivedLifetime) { + $receivedLifetime = $maxlifetime; + return true; + }, + ]); + $storage->gc(); + $this->assertEquals(300, $receivedLifetime); + } + + public function testGetSessionIDsThrowsException(): void + { + $storage = $this->createExternal(); + $this->expectException(Horde_SessionHandler_Exception::class); + $this->expectExceptionMessage('Driver does not support listing session IDs'); + $storage->getSessionIDs(); + } + + public function testCallbackReturnValuesArePropagated(): void + { + $storage = $this->createExternal([ + 'open' => fn() => false, + 'write' => fn() => false, + 'destroy' => fn() => false, + ]); + $this->assertFalse($storage->open('/tmp', 'sess')); + $this->assertFalse($storage->write('id', 'data')); + $this->assertFalse($storage->destroy('id')); + } +} diff --git a/test/unit/HandlerTest.php b/test/unit/HandlerTest.php index cb2c697..2797842 100644 --- a/test/unit/HandlerTest.php +++ b/test/unit/HandlerTest.php @@ -107,7 +107,7 @@ public function testReadReturnsDataFromStorage(): void public function testWriteSkipsWhenDataUnchanged(): void { $storage = $this->createMock(Horde_SessionHandler_Storage::class); - $storage->method('read')->willReturn('same-data'); + $storage->expects($this->once())->method('read')->willReturn('same-data'); // write should NOT be called because md5 signature matches $storage->expects($this->never())->method('write'); $handler = new Horde_SessionHandler($storage, ['noset' => true]); @@ -118,7 +118,7 @@ public function testWriteSkipsWhenDataUnchanged(): void public function testWriteProceedsWhenDataChanged(): void { $storage = $this->createMock(Horde_SessionHandler_Storage::class); - $storage->method('read')->willReturn('old-data'); + $storage->expects($this->once())->method('read')->willReturn('old-data'); $storage->expects($this->once()) ->method('write') ->with('test-id', 'new-data') @@ -131,7 +131,7 @@ public function testWriteProceedsWhenDataChanged(): void public function testWriteProceedsWhenChangedFlagSet(): void { $storage = $this->createMock(Horde_SessionHandler_Storage::class); - $storage->method('read')->willReturn('same-data'); + $storage->expects($this->once())->method('read')->willReturn('same-data'); $storage->expects($this->once()) ->method('write') ->with('test-id', 'same-data') @@ -145,7 +145,7 @@ public function testWriteProceedsWhenChangedFlagSet(): void public function testWriteReturnsFalseOnStorageFailure(): void { $storage = $this->createMock(Horde_SessionHandler_Storage::class); - $storage->method('read')->willReturn('old-data'); + $storage->expects($this->once())->method('read')->willReturn('old-data'); $storage->expects($this->once()) ->method('write') ->willReturn(false); @@ -157,7 +157,7 @@ public function testWriteReturnsFalseOnStorageFailure(): void public function testWriteSkipsMd5WhenNoMd5ParamSet(): void { $storage = $this->createMock(Horde_SessionHandler_Storage::class); - $storage->method('read')->willReturn('same-data'); + $storage->expects($this->once())->method('read')->willReturn('same-data'); // With no_md5, write should be skipped unless changed flag is set $storage->expects($this->never())->method('write'); $handler = new Horde_SessionHandler($storage, ['noset' => true, 'no_md5' => true]); @@ -168,7 +168,7 @@ public function testWriteSkipsMd5WhenNoMd5ParamSet(): void public function testWriteWithNoMd5AndChangedFlag(): void { $storage = $this->createMock(Horde_SessionHandler_Storage::class); - $storage->method('read')->willReturn('same-data'); + $storage->expects($this->once())->method('read')->willReturn('same-data'); $storage->expects($this->once()) ->method('write') ->willReturn(true); diff --git a/test/unit/HashtableUnitTest.php b/test/unit/HashtableUnitTest.php new file mode 100644 index 0000000..5582f8d --- /dev/null +++ b/test/unit/HashtableUnitTest.php @@ -0,0 +1,319 @@ +getMockBuilder(Horde_HashTable_Memory::class) + ->disableOriginalConstructor() + ->onlyMethods(['get', 'set', 'delete', 'exists', 'lock', 'unlock']) + ->getMock(); + } + + private function createStorage( + Horde_HashTable_Memory $hash, + bool $track = false + ): Horde_SessionHandler_Storage_Hashtable { + return new Horde_SessionHandler_Storage_Hashtable([ + 'hashtable' => $hash, + 'track' => $track, + ]); + } + + public function testConstructorRequiresHashtable(): void + { + $this->expectException(InvalidArgumentException::class); + new Horde_SessionHandler_Storage_Hashtable([]); + } + + public function testConstructorRequiresLockingSupport(): void + { + $hash = $this->createStub(Horde_HashTable_Base::class); + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('locking'); + new Horde_SessionHandler_Storage_Hashtable(['hashtable' => $hash]); + } + + public function testReadLocksAndReturnsData(): void + { + $hash = $this->createHashMock(); + $hash->expects($this->once())->method('lock')->with('sid'); + $hash->expects($this->once())->method('get')->with('sid')->willReturn('session-data'); + $hash->expects($this->never())->method('unlock'); + + $storage = $this->createStorage($hash); + $this->assertEquals('session-data', $storage->read('sid')); + } + + public function testReadUnlocksOnMiss(): void + { + $hash = $this->createHashMock(); + $hash->expects($this->once())->method('lock')->with('sid'); + $hash->expects($this->once())->method('get')->with('sid')->willReturn(false); + $hash->expects($this->once())->method('unlock')->with('sid'); + + $storage = $this->createStorage($hash); + $this->assertEquals('', $storage->read('sid')); + } + + public function testReadOnlySkipsLocking(): void + { + $hash = $this->createHashMock(); + $hash->expects($this->never())->method('lock'); + $hash->expects($this->once())->method('get')->with('sid')->willReturn('data'); + + $storage = $this->createStorage($hash); + $storage->readonly = true; + $this->assertEquals('data', $storage->read('sid')); + } + + public function testReadOnlyUnlocksNothingOnMiss(): void + { + $hash = $this->createHashMock(); + $hash->expects($this->never())->method('lock'); + $hash->expects($this->never())->method('unlock'); + $hash->expects($this->once())->method('get')->with('sid')->willReturn(false); + + $storage = $this->createStorage($hash); + $storage->readonly = true; + $this->assertEquals('', $storage->read('sid')); + } + + public function testCloseUnlocksCurrentSession(): void + { + $hash = $this->createHashMock(); + $hash->expects($this->once())->method('get')->willReturn('data'); + $hash->expects($this->once())->method('lock')->with('sid'); + $hash->expects($this->once())->method('unlock')->with('sid'); + + $storage = $this->createStorage($hash); + $storage->read('sid'); + $storage->close(); + } + + public function testCloseDoesNothingWithoutSession(): void + { + $hash = $this->createHashMock(); + $hash->expects($this->never())->method('unlock'); + + $storage = $this->createStorage($hash); + $storage->close(); + } + + public function testWriteWithoutTracking(): void + { + $hash = $this->createHashMock(); + $hash->expects($this->once())->method('set') + ->with('sid', 'data', $this->anything()) + ->willReturn(true); + + $storage = $this->createStorage($hash, track: false); + $this->assertTrue($storage->write('sid', 'data')); + } + + public function testWriteWithTrackingReplaceSucceeds(): void + { + $hash = $this->createHashMock(); + $hash->expects($this->once())->method('set') + ->with('sid', 'data', $this->callback(function ($opts) { + return !empty($opts['replace']); + })) + ->willReturn(true); + $hash->expects($this->never())->method('lock'); + + $storage = $this->createStorage($hash, track: true); + $this->assertTrue($storage->write('sid', 'data')); + } + + public function testWriteWithTrackingNewSession(): void + { + $hash = $this->createHashMock(); + $callCount = 0; + $hash->expects($this->exactly(3))->method('set') + ->willReturnCallback(function ($key, $val, $opts = []) use (&$callCount) { + $callCount++; + if ($callCount === 1) { + return false; + } + if ($callCount === 2) { + $this->assertEquals('sid', $key); + return true; + } + $this->assertEquals('horde_sessions_track_ht', $key); + return true; + }); + $hash->expects($this->once())->method('lock')->with('horde_sessions_track_ht'); + $hash->expects($this->once())->method('unlock')->with('horde_sessions_track_ht'); + $hash->expects($this->once())->method('get')->willReturn(false); + + $storage = $this->createStorage($hash, track: true); + $this->assertTrue($storage->write('sid', 'data')); + } + + public function testWriteReturnsFalseOnFailure(): void + { + $hash = $this->createHashMock(); + $hash->expects($this->once())->method('set')->willReturn(false); + + $storage = $this->createStorage($hash, track: false); + $this->assertFalse($storage->write('sid', 'data')); + } + + public function testDestroyDeletesAndUnlocks(): void + { + $hash = $this->createHashMock(); + $hash->expects($this->once())->method('delete')->with('sid')->willReturn(true); + $hash->expects($this->once())->method('unlock')->with('sid'); + + $storage = $this->createStorage($hash); + $this->assertTrue($storage->destroy('sid')); + } + + public function testDestroyReturnsFalseOnDeleteFailure(): void + { + $hash = $this->createHashMock(); + $hash->expects($this->once())->method('delete')->with('sid')->willReturn(false); + $hash->expects($this->once())->method('unlock')->with('sid'); + + $storage = $this->createStorage($hash); + $this->assertFalse($storage->destroy('sid')); + } + + public function testDestroyWithTrackingRemovesFromTrackList(): void + { + $hash = $this->createHashMock(); + $hash->expects($this->once())->method('delete')->willReturn(true); + $hash->expects($this->atLeastOnce())->method('unlock'); + $hash->expects($this->once())->method('lock')->with('horde_sessions_track_ht'); + $hash->expects($this->once())->method('get')->with('horde_sessions_track_ht') + ->willReturn(json_encode(['sid' => 1, 'other' => 1])); + $hash->expects($this->once())->method('set') + ->with( + 'horde_sessions_track_ht', + $this->callback(function ($val) { + $decoded = json_decode($val, true); + return isset($decoded['other']) && !isset($decoded['sid']); + }) + ) + ->willReturn(true); + + $storage = $this->createStorage($hash, track: true); + $this->assertTrue($storage->destroy('sid')); + } + + public function testGcAlwaysReturnsTrue(): void + { + $hash = new Horde_HashTable_Memory(); + $storage = new Horde_SessionHandler_Storage_Hashtable(['hashtable' => $hash]); + $this->assertTrue($storage->gc(300)); + $this->assertTrue($storage->gc(-1)); + } + + public function testGetSessionIDsThrowsWithoutTracking(): void + { + $hash = new Horde_HashTable_Memory(); + $storage = new Horde_SessionHandler_Storage_Hashtable(['hashtable' => $hash]); + $this->expectException(Horde_SessionHandler_Exception::class); + $storage->getSessionIDs(); + } + + public function testGetSessionIDsReturnsTrackedIds(): void + { + $hash = $this->createHashMock(); + $hash->expects($this->atLeastOnce())->method('lock'); + $hash->expects($this->atLeastOnce())->method('unlock'); + $hash->expects($this->atLeastOnce())->method('get')->willReturn(json_encode(['id1' => 1, 'id2' => 1])); + $hash->expects($this->atLeastOnce())->method('exists')->willReturn(true); + + $storage = $this->createStorage($hash, track: true); + $ids = $storage->getSessionIDs(); + sort($ids); + $this->assertEquals(['id1', 'id2'], $ids); + } + + public function testTrackGCRemovesExpiredSessions(): void + { + $hash = $this->createHashMock(); + $hash->expects($this->atLeastOnce())->method('lock'); + $hash->expects($this->atLeastOnce())->method('unlock'); + $hash->expects($this->atLeastOnce())->method('get')->with('horde_sessions_track_ht') + ->willReturn(json_encode(['alive' => 1, 'dead' => 1])); + $hash->expects($this->atLeastOnce())->method('exists')->willReturnCallback(function ($key) { + return $key === 'alive'; + }); + $hash->expects($this->once())->method('set') + ->with( + 'horde_sessions_track_ht', + $this->callback(function ($val) { + $decoded = json_decode($val, true); + return isset($decoded['alive']) && !isset($decoded['dead']); + }) + ) + ->willReturn(true); + + $storage = $this->createStorage($hash, track: true); + $storage->trackGC(); + } + + public function testTrackGCHandlesExceptionGracefully(): void + { + $hash = $this->createHashMock(); + $hash->expects($this->once())->method('lock') + ->willThrowException(new Horde_HashTable_Exception('fail')); + + $storage = $this->createStorage($hash, track: true); + $storage->trackGC(); + $this->assertTrue(true); + } + + public function testTrackGCNoOpWhenNoTrackedIds(): void + { + $hash = $this->createHashMock(); + $hash->expects($this->once())->method('lock'); + $hash->expects($this->once())->method('unlock'); + $hash->expects($this->once())->method('get')->willReturn(false); + $hash->expects($this->never())->method('set'); + + $storage = $this->createStorage($hash, track: true); + $storage->trackGC(); + } + + public function testTrackGCNoOpWhenAllSessionsAlive(): void + { + $hash = $this->createHashMock(); + $hash->expects($this->once())->method('lock'); + $hash->expects($this->once())->method('unlock'); + $hash->expects($this->once())->method('get')->willReturn(json_encode(['a' => 1, 'b' => 1])); + $hash->expects($this->atLeastOnce())->method('exists')->willReturn(true); + $hash->expects($this->never())->method('set'); + + $storage = $this->createStorage($hash, track: true); + $storage->trackGC(); + } +} diff --git a/test/unit/SqlUnitTest.php b/test/unit/SqlUnitTest.php new file mode 100644 index 0000000..36b6137 --- /dev/null +++ b/test/unit/SqlUnitTest.php @@ -0,0 +1,359 @@ +createMock(TestDbAdapter::class); + } + + private function createDbStub(): TestDbAdapter + { + return $this->createStub(TestDbAdapter::class); + } + + private function createStorage(?TestDbAdapter $db = null, string $table = 'horde_sessionhandler'): Horde_SessionHandler_Storage_Sql + { + $db ??= $this->createDbMock(); + return new Horde_SessionHandler_Storage_Sql([ + 'db' => $db, + 'table' => $table, + ]); + } + + private function stubColumnForRead(TestDbAdapter $db, string $binaryResult): void + { + $column = $this->createStub(Horde_Db_Adapter_Base_Column::class); + $column->method('binaryToString')->willReturn($binaryResult); + $db->expects($this->once())->method('columns')->willReturn(['session_data' => $column]); + } + + public function testConstructorRequiresDb(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Missing db parameter'); + new Horde_SessionHandler_Storage_Sql([]); + } + + public function testConstructorUsesDefaultTableName(): void + { + $db = $this->createDbMock(); + $storage = new Horde_SessionHandler_Storage_Sql(['db' => $db]); + + $db->expects($this->once())->method('transactionStarted')->willReturn(false); + $db->expects($this->once())->method('beginDbTransaction'); + $this->stubColumnForRead($db, ''); + $db->expects($this->once())->method('selectValue') + ->with( + $this->stringContains('horde_sessionhandler'), + $this->anything() + ) + ->willReturn(null); + + $storage->read('test'); + } + + public function testConstructorAcceptsCustomTableName(): void + { + $db = $this->createDbMock(); + $storage = new Horde_SessionHandler_Storage_Sql([ + 'db' => $db, + 'table' => 'custom_sessions', + ]); + + $db->expects($this->once())->method('transactionStarted')->willReturn(false); + $db->expects($this->once())->method('beginDbTransaction'); + $this->stubColumnForRead($db, ''); + $db->expects($this->once())->method('selectValue') + ->with( + $this->stringContains('custom_sessions'), + $this->anything() + ) + ->willReturn(null); + + $storage->read('test'); + } + + public function testOpenReturnsTrue(): void + { + $db = $this->createDbStub(); + $storage = $this->createStorage($db); + $this->assertTrue($storage->open('/tmp', 'sess')); + } + + public function testCloseCommitsOpenTransaction(): void + { + $db = $this->createDbMock(); + $db->expects($this->once())->method('transactionStarted')->willReturn(true); + $db->expects($this->once())->method('commitDbTransaction'); + + $storage = $this->createStorage($db); + $this->assertTrue($storage->close()); + } + + public function testCloseReturnsTrueWithNoTransaction(): void + { + $db = $this->createDbMock(); + $db->expects($this->once())->method('transactionStarted')->willReturn(false); + $db->expects($this->never())->method('commitDbTransaction'); + + $storage = $this->createStorage($db); + $this->assertTrue($storage->close()); + } + + public function testCloseReturnsFalseOnCommitError(): void + { + $db = $this->createDbStub(); + $db->method('transactionStarted')->willReturn(true); + $db->method('commitDbTransaction') + ->willThrowException(new Horde_Db_Exception('commit failed')); + + $storage = $this->createStorage($db); + $this->assertFalse($storage->close()); + } + + public function testReadBeginsTransactionAndReturnsData(): void + { + $db = $this->createDbMock(); + $db->expects($this->once())->method('transactionStarted')->willReturn(false); + $db->expects($this->once())->method('beginDbTransaction'); + $this->stubColumnForRead($db, 'session-data'); + $db->expects($this->once())->method('selectValue')->willReturn('binary-blob'); + + $storage = $this->createStorage($db); + $this->assertEquals('session-data', $storage->read('sid')); + } + + public function testReadDoesNotRestartExistingTransaction(): void + { + $db = $this->createDbMock(); + $db->expects($this->once())->method('transactionStarted')->willReturn(true); + $db->expects($this->never())->method('beginDbTransaction'); + $this->stubColumnForRead($db, 'data'); + $db->expects($this->once())->method('selectValue')->willReturn('blob'); + + $storage = $this->createStorage($db); + $storage->read('sid'); + } + + public function testReadReturnsEmptyStringOnDbError(): void + { + $db = $this->createDbStub(); + $db->method('transactionStarted')->willReturn(false); + $db->method('beginDbTransaction'); + $db->method('columns') + ->willThrowException(new Horde_Db_Exception('db error')); + + $storage = $this->createStorage($db); + $this->assertEquals('', $storage->read('sid')); + } + + public function testWriteInsertsNewSession(): void + { + $db = $this->createDbMock(); + $db->expects($this->once())->method('isActive')->willReturn(true); + $db->expects($this->once())->method('selectValue')->willReturn(null); + $db->expects($this->once())->method('insertBlob') + ->with( + 'horde_sessionhandler', + $this->callback(function ($fields) { + return $fields['session_id'] === 'sid' + && $fields['session_data'] instanceof Horde_Db_Value_Binary + && isset($fields['session_lastmodified']); + }), + null, + 'sid' + ); + $db->expects($this->once())->method('commitDbTransaction'); + + $storage = $this->createStorage($db); + $this->assertTrue($storage->write('sid', 'data')); + } + + public function testWriteUpdatesExistingSession(): void + { + $db = $this->createDbMock(); + $db->expects($this->once())->method('isActive')->willReturn(true); + $db->expects($this->once())->method('selectValue')->willReturn(1); + $db->expects($this->once())->method('updateBlob') + ->with( + 'horde_sessionhandler', + $this->callback(function ($fields) { + return $fields['session_data'] instanceof Horde_Db_Value_Binary + && isset($fields['session_lastmodified']); + }), + $this->callback(function ($where) { + return $where[0] === 'session_id = ?' && $where[1] === ['sid']; + }) + ); + $db->expects($this->once())->method('commitDbTransaction'); + + $storage = $this->createStorage($db); + $this->assertTrue($storage->write('sid', 'updated-data')); + } + + public function testWriteReconnectsIfNotActive(): void + { + $db = $this->createDbMock(); + $db->expects($this->once())->method('isActive')->willReturn(false); + $db->expects($this->once())->method('reconnect'); + $db->expects($this->once())->method('beginDbTransaction'); + $db->expects($this->once())->method('selectValue')->willReturn(null); + $db->expects($this->once())->method('insertBlob'); + $db->expects($this->once())->method('commitDbTransaction'); + + $storage = $this->createStorage($db); + $this->assertTrue($storage->write('sid', 'data')); + } + + public function testWriteReturnsFalseOnExistenceCheckError(): void + { + $db = $this->createDbStub(); + $db->method('isActive')->willReturn(true); + $db->method('selectValue') + ->willThrowException(new Horde_Db_Exception('query failed')); + + $storage = $this->createStorage($db); + $this->assertFalse($storage->write('sid', 'data')); + } + + public function testWriteRollsBackOnInsertError(): void + { + $db = $this->createDbMock(); + $db->expects($this->once())->method('isActive')->willReturn(true); + $db->expects($this->once())->method('selectValue')->willReturn(null); + $db->expects($this->once())->method('insertBlob') + ->willThrowException(new Horde_Db_Exception('insert failed')); + $db->expects($this->once())->method('rollbackDbTransaction'); + + $storage = $this->createStorage($db); + $this->assertFalse($storage->write('sid', 'data')); + } + + public function testWriteHandlesRollbackFailureGracefully(): void + { + $db = $this->createDbStub(); + $db->method('isActive')->willReturn(true); + $db->method('selectValue')->willReturn(null); + $db->method('insertBlob') + ->willThrowException(new Horde_Db_Exception('insert failed')); + $db->method('rollbackDbTransaction') + ->willThrowException(new Horde_Db_Exception('rollback failed')); + + $storage = $this->createStorage($db); + $this->assertFalse($storage->write('sid', 'data')); + } + + public function testDestroyDeletesAndCommits(): void + { + $db = $this->createDbMock(); + $db->expects($this->once())->method('delete') + ->with( + $this->stringContains('DELETE FROM horde_sessionhandler WHERE session_id = ?'), + ['sid'] + ); + $db->expects($this->once())->method('commitDbTransaction'); + + $storage = $this->createStorage($db); + $this->assertTrue($storage->destroy('sid')); + } + + public function testDestroyReturnsFalseOnError(): void + { + $db = $this->createDbStub(); + $db->method('delete') + ->willThrowException(new Horde_Db_Exception('delete failed')); + + $storage = $this->createStorage($db); + $this->assertFalse($storage->destroy('sid')); + } + + public function testGcDeletesExpiredSessions(): void + { + $db = $this->createDbMock(); + $db->expects($this->once())->method('delete') + ->with( + $this->stringContains('session_lastmodified'), + $this->callback(function ($values) { + return is_array($values) && count($values) === 1 && is_int($values[0]); + }) + ); + + $storage = $this->createStorage($db); + $this->assertTrue($storage->gc(300)); + } + + public function testGcReturnsFalseOnError(): void + { + $db = $this->createDbStub(); + $db->method('delete') + ->willThrowException(new Horde_Db_Exception('gc failed')); + + $storage = $this->createStorage($db); + $this->assertFalse($storage->gc(300)); + } + + public function testGetSessionIDsReturnsActiveSessionIds(): void + { + $db = $this->createDbMock(); + $db->expects($this->once())->method('selectValues') + ->with( + $this->stringContains('session_id'), + $this->callback(function ($values) { + return is_array($values) && count($values) === 1; + }) + ) + ->willReturn(['id1', 'id2', 'id3']); + + $storage = $this->createStorage($db); + $this->assertEquals(['id1', 'id2', 'id3'], $storage->getSessionIDs()); + } + + public function testGetSessionIDsReturnsEmptyOnError(): void + { + $db = $this->createDbStub(); + $db->method('selectValues') + ->willThrowException(new Horde_Db_Exception('query failed')); + + $storage = $this->createStorage($db); + $this->assertEquals([], $storage->getSessionIDs()); + } +} diff --git a/test/unit/StackUnitTest.php b/test/unit/StackUnitTest.php new file mode 100644 index 0000000..06046c8 --- /dev/null +++ b/test/unit/StackUnitTest.php @@ -0,0 +1,198 @@ +createStub(Horde_SessionHandler_Storage::class); + foreach ($methods as $method => $return) { + $stub->method($method)->willReturn($return); + } + return $stub; + } + + public function testOpenCallsAllBackends(): void + { + $s1 = $this->createMock(Horde_SessionHandler_Storage::class); + $s2 = $this->createMock(Horde_SessionHandler_Storage::class); + $s1->expects($this->once())->method('open')->with('/tmp', 'sess')->willReturn(true); + $s2->expects($this->once())->method('open')->with('/tmp', 'sess')->willReturn(true); + + $stack = new Horde_SessionHandler_Storage_Stack(['stack' => [$s1, $s2]]); + $this->assertTrue($stack->open('/tmp', 'sess')); + } + + public function testOpenReturnsFalseIfAnyBackendFails(): void + { + $s1 = $this->createStub(Horde_SessionHandler_Storage::class); + $s1->method('open')->willReturn(false); + $s2 = $this->createStub(Horde_SessionHandler_Storage::class); + $s2->method('open')->willReturn(true); + + $stack = new Horde_SessionHandler_Storage_Stack(['stack' => [$s1, $s2]]); + $this->assertFalse($stack->open('/tmp', 'sess')); + } + + public function testCloseCallsAllBackends(): void + { + $s1 = $this->createMock(Horde_SessionHandler_Storage::class); + $s2 = $this->createMock(Horde_SessionHandler_Storage::class); + $s1->expects($this->once())->method('close')->willReturn(true); + $s2->expects($this->once())->method('close')->willReturn(true); + + $stack = new Horde_SessionHandler_Storage_Stack(['stack' => [$s1, $s2]]); + $this->assertTrue($stack->close()); + } + + public function testCloseReturnsFalseIfAnyBackendFails(): void + { + $s1 = $this->createStub(Horde_SessionHandler_Storage::class); + $s1->method('close')->willReturn(true); + $s2 = $this->createStub(Horde_SessionHandler_Storage::class); + $s2->method('close')->willReturn(false); + + $stack = new Horde_SessionHandler_Storage_Stack(['stack' => [$s1, $s2]]); + $this->assertFalse($stack->close()); + } + + public function testReadReturnsDataFromLastBackendInStack(): void + { + $s1 = $this->createStub(Horde_SessionHandler_Storage::class); + $s1->method('read')->willReturn('data-from-cache'); + $s2 = $this->createStub(Horde_SessionHandler_Storage::class); + $s2->method('read')->willReturn('data-from-master'); + + $stack = new Horde_SessionHandler_Storage_Stack(['stack' => [$s1, $s2]]); + // Stack reads in order; last result wins + $this->assertEquals('data-from-master', $stack->read('id')); + } + + public function testReadStopsOnFalse(): void + { + $s1 = $this->createMock(Horde_SessionHandler_Storage::class); + $s1->expects($this->once())->method('read')->with('id')->willReturn(false); + $s2 = $this->createMock(Horde_SessionHandler_Storage::class); + $s2->expects($this->never())->method('read'); + + $stack = new Horde_SessionHandler_Storage_Stack(['stack' => [$s1, $s2]]); + $this->assertFalse($stack->read('id')); + } + + public function testWriteWritesInReverseOrder(): void + { + // Master is last in the stack array, so written first (reverse order) + $callOrder = []; + $s1 = $this->createMock(Horde_SessionHandler_Storage::class); + $s1->expects($this->once())->method('write') + ->willReturnCallback(function () use (&$callOrder) { + $callOrder[] = 'cache'; + return true; + }); + $s2 = $this->createMock(Horde_SessionHandler_Storage::class); + $s2->expects($this->once())->method('write') + ->willReturnCallback(function () use (&$callOrder) { + $callOrder[] = 'master'; + return true; + }); + + $stack = new Horde_SessionHandler_Storage_Stack(['stack' => [$s1, $s2]]); + $this->assertTrue($stack->write('id', 'data')); + $this->assertEquals(['master', 'cache'], $callOrder); + } + + public function testWriteReturnsFalseIfMasterFails(): void + { + $s1 = $this->createStub(Horde_SessionHandler_Storage::class); + $s1->method('write')->willReturn(true); + // Master (last in stack) fails + $s2 = $this->createStub(Horde_SessionHandler_Storage::class); + $s2->method('write')->willReturn(false); + + $stack = new Horde_SessionHandler_Storage_Stack(['stack' => [$s1, $s2]]); + $this->assertFalse($stack->write('id', 'data')); + } + + public function testWriteInvalidatesCacheOnNonMasterFailure(): void + { + // s1 (cache) fails write, s2 (master) succeeds + $s1 = $this->createMock(Horde_SessionHandler_Storage::class); + $s1->expects($this->once())->method('write')->willReturn(false); + // When cache write fails, destroy is called to invalidate + $s1->expects($this->once())->method('destroy')->with('id'); + + $s2 = $this->createStub(Horde_SessionHandler_Storage::class); + $s2->method('write')->willReturn(true); + + $stack = new Horde_SessionHandler_Storage_Stack(['stack' => [$s1, $s2]]); + // Returns true because master succeeded + $this->assertTrue($stack->write('id', 'data')); + } + + public function testDestroyReportsOnlyMasterResult(): void + { + // Master succeeds, cache fails — overall success + $s1 = $this->createStub(Horde_SessionHandler_Storage::class); + $s1->method('destroy')->willReturn(false); + $s2 = $this->createStub(Horde_SessionHandler_Storage::class); + $s2->method('destroy')->willReturn(true); + + $stack = new Horde_SessionHandler_Storage_Stack(['stack' => [$s1, $s2]]); + $this->assertTrue($stack->destroy('id')); + } + + public function testDestroyReturnsFalseIfMasterFails(): void + { + $s1 = $this->createStub(Horde_SessionHandler_Storage::class); + $s1->method('destroy')->willReturn(true); + $s2 = $this->createStub(Horde_SessionHandler_Storage::class); + $s2->method('destroy')->willReturn(false); + + $stack = new Horde_SessionHandler_Storage_Stack(['stack' => [$s1, $s2]]); + $this->assertFalse($stack->destroy('id')); + } + + public function testGcReturnsResultFromMasterOnly(): void + { + $s1 = $this->createMock(Horde_SessionHandler_Storage::class); + $s1->expects($this->once())->method('gc')->with(300)->willReturn(false); + // Master is last — its result is returned + $s2 = $this->createMock(Horde_SessionHandler_Storage::class); + $s2->expects($this->once())->method('gc')->with(300)->willReturn(true); + + $stack = new Horde_SessionHandler_Storage_Stack(['stack' => [$s1, $s2]]); + $this->assertTrue($stack->gc(300)); + } + + public function testGetSessionIDsDelegatestoMaster(): void + { + $s1 = $this->createMock(Horde_SessionHandler_Storage::class); + $s1->expects($this->never())->method('getSessionIDs'); + $s2 = $this->createMock(Horde_SessionHandler_Storage::class); + $s2->expects($this->once())->method('getSessionIDs')->willReturn(['a', 'b']); + + $stack = new Horde_SessionHandler_Storage_Stack(['stack' => [$s1, $s2]]); + $this->assertEquals(['a', 'b'], $stack->getSessionIDs()); + } +}