diff --git a/lib/Horde/Auth.php b/lib/Horde/Auth.php index 30f1a8d..a0c7624 100644 --- a/lib/Horde/Auth.php +++ b/lib/Horde/Auth.php @@ -30,7 +30,7 @@ class Horde_Auth * Authentication success */ public const REASON_SUCCESS = 0; - + /** * Authentication failure reason: Bad username and/or password */ @@ -181,9 +181,9 @@ public static function getCryptedPassword( $j = 5; } $p[] = self::_toAPRMD5( - (ord($binary[$i]) << 16) | - (ord($binary[$k]) << 8) | - (ord($binary[$j])), + (ord($binary[$i]) << 16) + | (ord($binary[$k]) << 8) + | (ord($binary[$j])), 5 ); } @@ -359,13 +359,13 @@ public static function genRandomPassword() { /* Alternate consonant and vowel random chars with two random numbers * at the end. This should produce a fairly pronounceable password. */ - return substr(self::CONSONANTS, mt_rand(0, strlen(self::CONSONANTS) - 1), 1) . - substr(self::VOWELS, mt_rand(0, strlen(self::VOWELS) - 1), 1) . - substr(self::CONSONANTS, mt_rand(0, strlen(self::CONSONANTS) - 1), 1) . - substr(self::VOWELS, mt_rand(0, strlen(self::VOWELS) - 1), 1) . - substr(self::CONSONANTS, mt_rand(0, strlen(self::CONSONANTS) - 1), 1) . - substr(self::NUMBERS, mt_rand(0, strlen(self::NUMBERS) - 1), 1) . - substr(self::NUMBERS, mt_rand(0, strlen(self::NUMBERS) - 1), 1); + return substr(self::CONSONANTS, mt_rand(0, strlen(self::CONSONANTS) - 1), 1) + . substr(self::VOWELS, mt_rand(0, strlen(self::VOWELS) - 1), 1) + . substr(self::CONSONANTS, mt_rand(0, strlen(self::CONSONANTS) - 1), 1) + . substr(self::VOWELS, mt_rand(0, strlen(self::VOWELS) - 1), 1) + . substr(self::CONSONANTS, mt_rand(0, strlen(self::CONSONANTS) - 1), 1) + . substr(self::NUMBERS, mt_rand(0, strlen(self::NUMBERS) - 1), 1) + . substr(self::NUMBERS, mt_rand(0, strlen(self::NUMBERS) - 1), 1); } /** @@ -407,12 +407,12 @@ public static function genRandomPassword() public static function checkPasswordPolicy($password, array $policy) { // Check max/min lengths if specified in the policy. - if (isset($policy['minLength']) && - strlen($password) < $policy['minLength']) { + if (isset($policy['minLength']) + && strlen($password) < $policy['minLength']) { throw new Horde_Auth_Exception(sprintf(Horde_Auth_Translation::t("The password must be at least %d characters long!"), $policy['minLength'])); } - if (isset($policy['maxLength']) && - strlen($password) > $policy['maxLength']) { + if (isset($policy['maxLength']) + && strlen($password) > $policy['maxLength']) { throw new Horde_Auth_Exception(sprintf(Horde_Auth_Translation::t("The password is too long; passwords may not be more than %d characters long!"), $policy['maxLength'])); } @@ -496,8 +496,8 @@ public static function checkPasswordSimilarity( ) { // Check for pass == dict, simple reverse strings, etc. foreach ($dict as $test) { - if ((strcasecmp($password, $test) == 0) || - (strcasecmp($password, strrev($test)) == 0)) { + if ((strcasecmp($password, $test) == 0) + || (strcasecmp($password, strrev($test)) == 0)) { throw new Horde_Auth_Exception(Horde_Auth_Translation::t("The password is too simple to guess.")); } } diff --git a/lib/Horde/Auth/Base.php b/lib/Horde/Auth/Base.php index 5d4222a..7fc6049 100644 --- a/lib/Horde/Auth/Base.php +++ b/lib/Horde/Auth/Base.php @@ -149,8 +149,8 @@ public function authenticate($userId, $credentials, $login = true) try { $this->_credentials['userId'] = $userId; - if (($this->hasCapability('lock')) && - $this->isLocked($userId)) { + if (($this->hasCapability('lock')) + && $this->isLocked($userId)) { $details = $this->isLocked($userId, true); if ($details['lock_timeout'] == Horde_Lock::PERMANENT) { $message = Horde_Auth_Translation::t("Your account has been permanently locked"); @@ -167,10 +167,10 @@ public function authenticate($userId, $credentials, $login = true) } return true; } catch (Horde_Auth_Exception $e) { - if (($code = $e->getCode()) && - $code != Horde_Auth::REASON_MESSAGE) { - if (($code == Horde_Auth::REASON_BADLOGIN) && - $this->hasCapability('badlogincount')) { + if (($code = $e->getCode()) + && $code != Horde_Auth::REASON_MESSAGE) { + if (($code == Horde_Auth::REASON_BADLOGIN) + && $this->hasCapability('badlogincount')) { $this->_badLogin($userId); } $this->setError($code, $e->getMessage()); @@ -362,9 +362,9 @@ protected function _badLogin($userId) ['action' => 'login_failed', 'who' => $userId] ); $history_log = $this->_history_api->getHistory($history_identifier); - if ($this->_params['login_block_count'] > 0 && - $this->_params['login_block_count'] <= $history_log->count() && - $this->hasCapability('lock')) { + if ($this->_params['login_block_count'] > 0 + && $this->_params['login_block_count'] <= $history_log->count() + && $this->hasCapability('lock')) { $this->lockUser($userId, $this->_params['login_block_time']); } } catch (Horde_History_Exception $e) { diff --git a/lib/Horde/Auth/Cyrsql.php b/lib/Horde/Auth/Cyrsql.php index 23a6aa8..c75e096 100644 --- a/lib/Horde/Auth/Cyrsql.php +++ b/lib/Horde/Auth/Cyrsql.php @@ -144,8 +144,8 @@ class Horde_Auth_Cyrsql extends Horde_Auth_Sql */ public function __construct(array $params = []) { - if (!isset($params['imap']) || - !($params['imap'] instanceof Horde_Imap_Client_Base)) { + if (!isset($params['imap']) + || !($params['imap'] instanceof Horde_Imap_Client_Base)) { throw new InvalidArgumentException('Missing imap parameter.'); } $this->_imap = $params['imap']; @@ -172,8 +172,8 @@ public function __construct(array $params = []) */ protected function _authenticate($userId, $credentials) { - if (!empty($this->_params['domain_field']) && - ($this->_params['domain_field'] != 'none')) { + if (!empty($this->_params['domain_field']) + && ($this->_params['domain_field'] != 'none')) { /* Build the SQL query with domain. */ $query = sprintf( 'SELECT * FROM %s WHERE %s = ? AND %s = ?', @@ -198,21 +198,21 @@ protected function _authenticate($userId, $credentials) throw new Horde_Auth_Exception('', Horde_Auth::REASON_FAILED); } - if (!$row || - !$this->_comparePasswords($row[$this->_params['password_field']], $credentials['password'])) { + if (!$row + || !$this->_comparePasswords($row[$this->_params['password_field']], $credentials['password'])) { throw new Horde_Auth_Exception('', Horde_Auth::REASON_BADLOGIN); } $now = time(); - if (!empty($this->_params['hard_expiration_field']) && - !empty($row[$this->_params['hard_expiration_field']]) && - ($now > $row[$this->_params['hard_expiration_field']])) { + if (!empty($this->_params['hard_expiration_field']) + && !empty($row[$this->_params['hard_expiration_field']]) + && ($now > $row[$this->_params['hard_expiration_field']])) { throw new Horde_Auth_Exception('', Horde_Auth::REASON_EXPIRED); } - if (!empty($this->_params['soft_expiration_field']) && - !empty($row[$this->_params['soft_expiration_field']]) && - ($now > $row[$this->_params['soft_expiration_field']])) { + if (!empty($this->_params['soft_expiration_field']) + && !empty($row[$this->_params['soft_expiration_field']]) + && ($now > $row[$this->_params['soft_expiration_field']])) { $this->setCredential('change', true); } } @@ -227,8 +227,8 @@ protected function _authenticate($userId, $credentials) */ public function addUser($userId, $credentials) { - if (!empty($this->_params['domain_field']) && - ($this->_params['domain_field'] != 'none')) { + if (!empty($this->_params['domain_field']) + && ($this->_params['domain_field'] != 'none')) { [$name, $domain] = explode('@', $userId); $query = sprintf( @@ -267,8 +267,8 @@ public function addUser($userId, $credentials) try { $this->_imap->createMailbox($mailbox); $this->_imap->setACL($mailbox, $this->_params['cyradmin'], ['rights' => 'lrswipcda']); - if (isset($this->_params['quota']) && - ($this->_params['quota'] >= 0)) { + if (isset($this->_params['quota']) + && ($this->_params['quota'] >= 0)) { $this->_imap->setQuota($mailbox, ['storage' => $this->_params['quota']]); } } catch (Horde_Imap_Client_Exception $e) { @@ -293,8 +293,8 @@ public function addUser($userId, $credentials) */ public function removeUser($userId) { - if (!empty($this->_params['domain_field']) && - ($this->_params['domain_field'] != 'none')) { + if (!empty($this->_params['domain_field']) + && ($this->_params['domain_field'] != 'none')) { [$name, $domain] = explode('@', $userId); /* Build the SQL query. */ @@ -342,8 +342,8 @@ public function removeUser($userId) */ public function listUsers($sort = false) { - if (!empty($this->_params['domain_field']) && - ($this->_params['domain_field'] != 'none')) { + if (!empty($this->_params['domain_field']) + && ($this->_params['domain_field'] != 'none')) { /* Build the SQL query with domain. */ $query = sprintf( 'SELECT %s, %s FROM %s', @@ -371,8 +371,8 @@ public function listUsers($sort = false) /* Loop through and build return array. */ $users = []; - if (!empty($this->_params['domain_field']) && - ($this->_params['domain_field'] != 'none')) { + if (!empty($this->_params['domain_field']) + && ($this->_params['domain_field'] != 'none')) { foreach ($result as $ar) { if (!in_array($ar[$this->_params['username_field']], $this->_params['hidden_accounts'])) { $users[] = $ar[$this->_params['username_field']] . '@' . $ar[$this->_params['domain_field']]; @@ -399,8 +399,8 @@ public function listUsers($sort = false) */ public function updateUser($oldID, $newID, $credentials) { - if (!empty($this->_params['domain_field']) && - ($this->_params['domain_field'] != 'none')) { + if (!empty($this->_params['domain_field']) + && ($this->_params['domain_field'] != 'none')) { [$name, $domain] = explode('@', $oldID); /* Build the SQL query with domain. */ $query = sprintf( diff --git a/lib/Horde/Auth/Http.php b/lib/Horde/Auth/Http.php index 179c032..6c70cdd 100644 --- a/lib/Horde/Auth/Http.php +++ b/lib/Horde/Auth/Http.php @@ -88,8 +88,8 @@ public function __construct(array $params = []) */ protected function _authenticate($userId, $credentials) { - if (empty($credentials['password']) || - empty($this->_users[$userId])) { + if (empty($credentials['password']) + || empty($this->_users[$userId])) { throw new Horde_Auth_Exception('', Horde_Auth::REASON_BADLOGIN); } @@ -122,8 +122,8 @@ public function listUsers($sort = false) */ public function transparent() { - if (empty($_SERVER['PHP_AUTH_USER']) || - empty($_SERVER['PHP_AUTH_PW'])) { + if (empty($_SERVER['PHP_AUTH_USER']) + || empty($_SERVER['PHP_AUTH_PW'])) { return false; } diff --git a/lib/Horde/Auth/Ldap.php b/lib/Horde/Auth/Ldap.php index b2d3c08..07b1e7c 100644 --- a/lib/Horde/Auth/Ldap.php +++ b/lib/Horde/Auth/Ldap.php @@ -227,11 +227,11 @@ protected function _authenticate($userId, $credentials) if ($this->_params['password_expiration'] == 'yes') { $shadow = $this->_lookupShadow($dn); - if ($shadow['shadowmax'] && $shadow['shadowlastchange'] && - $shadow['shadowwarning']) { + if ($shadow['shadowmax'] && $shadow['shadowlastchange'] + && $shadow['shadowwarning']) { $today = floor(time() / 86400); - $toexpire = $shadow['shadowlastchange'] + - $shadow['shadowmax'] - $today; + $toexpire = $shadow['shadowlastchange'] + + $shadow['shadowmax'] - $today; $warnday = $shadow['shadowlastchange'] + $shadow['shadowmax'] - $shadow['shadowwarning']; if ($today >= $warnday) { @@ -368,9 +368,9 @@ public function updateUser( /* If shadowmin hasn't yet expired only change when we are administrator */ - if ($shadow['shadowlastchange'] && - $shadow['shadowmin'] && - ($shadow['shadowlastchange'] + $shadow['shadowmin'] > (time() / 86400))) { + if ($shadow['shadowlastchange'] + && $shadow['shadowmin'] + && ($shadow['shadowlastchange'] + $shadow['shadowmin'] > (time() / 86400))) { throw new Horde_Auth_Exception('Minimum password age has not yet expired'); } diff --git a/lib/Horde/Auth/Msad.php b/lib/Horde/Auth/Msad.php index e1c77f2..71f468e 100644 --- a/lib/Horde/Auth/Msad.php +++ b/lib/Horde/Auth/Msad.php @@ -96,8 +96,8 @@ public function addUser($accountName, $credentials) $entry['objectclass'][2] = "organizationalPerson"; $entry['objectclass'][3] = "user"; - $entry['description'] = (isset($credentials['description'])) ? - $credentials['description'] : 'New horde user'; + $entry['description'] = (isset($credentials['description'])) + ? $credentials['description'] : 'New horde user'; if ($this->_params['ssl']) { $entry["AccountDisabled"] = false; diff --git a/lib/Horde/Auth/Passwd.php b/lib/Horde/Auth/Passwd.php index c43b140..b7d8e23 100644 --- a/lib/Horde/Auth/Passwd.php +++ b/lib/Horde/Auth/Passwd.php @@ -272,8 +272,8 @@ protected function _authenticate($userId, $credentials) throw new Horde_Auth_Exception('', Horde_Auth::REASON_FAILED); } - if (!isset($this->_users[$userId]) || - !$this->_comparePasswords($this->_users[$userId]['password'], $credentials['password'])) { + if (!isset($this->_users[$userId]) + || !$this->_comparePasswords($this->_users[$userId]['password'], $credentials['password'])) { throw new Horde_Auth_Exception('', Horde_Auth::REASON_BADLOGIN); } diff --git a/lib/Horde/Auth/Sql.php b/lib/Horde/Auth/Sql.php index a02f500..af3d152 100644 --- a/lib/Horde/Auth/Sql.php +++ b/lib/Horde/Auth/Sql.php @@ -106,13 +106,13 @@ public function __construct(array $params = []) parent::__construct($params); /* Only allow limits when there is a storage configured */ - if ((empty($params['soft_expiration_field'])) && - ($params['soft_expiration_window'] > 0)) { + if ((empty($params['soft_expiration_field'])) + && ($params['soft_expiration_window'] > 0)) { throw new InvalidArgumentException('You cannot set [soft_expiration_window] without [soft_expiration_field].'); } - if (($params['hard_expiration_field'] == '') && - ($params['hard_expiration_window'] > 0)) { + if (($params['hard_expiration_field'] == '') + && ($params['hard_expiration_window'] > 0)) { throw new InvalidArgumentException('You cannot set [hard_expiration_window] without [hard_expiration_field].'); } @@ -142,21 +142,21 @@ protected function _authenticate($userId, $credentials) throw new Horde_Auth_Exception('', Horde_Auth::REASON_FAILED); } - if (!$row || - !$this->_comparePasswords($row[$this->_params['password_field']], $credentials['password'])) { + if (!$row + || !$this->_comparePasswords($row[$this->_params['password_field']], $credentials['password'])) { throw new Horde_Auth_Exception('', Horde_Auth::REASON_BADLOGIN); } $now = time(); - if (!empty($this->_params['hard_expiration_field']) && - !empty($row[$this->_params['hard_expiration_field']]) && - ($now > $row[$this->_params['hard_expiration_field']])) { + if (!empty($this->_params['hard_expiration_field']) + && !empty($row[$this->_params['hard_expiration_field']]) + && ($now > $row[$this->_params['hard_expiration_field']])) { throw new Horde_Auth_Exception('', Horde_Auth::REASON_EXPIRED); } - if (!empty($this->_params['soft_expiration_field']) && - !empty($row[$this->_params['soft_expiration_field']]) && - ($now > $row[$this->_params['soft_expiration_field']])) { + if (!empty($this->_params['soft_expiration_field']) + && !empty($row[$this->_params['soft_expiration_field']]) + && ($now > $row[$this->_params['soft_expiration_field']])) { $this->setCredential('change', true); $this->setCredential('expire', $now); } diff --git a/lib/Horde/Auth/X509.php b/lib/Horde/Auth/X509.php index 8172c07..a071c3e 100644 --- a/lib/Horde/Auth/X509.php +++ b/lib/Horde/Auth/X509.php @@ -98,16 +98,16 @@ public function transparent() throw new Horde_Auth_Exception('SSL not enabled on server.'); } - if (empty($_SERVER[$this->_params['username_field']]) || - empty($_SERVER[$this->_params['certificate_field']])) { + if (empty($_SERVER[$this->_params['username_field']]) + || empty($_SERVER[$this->_params['certificate_field']])) { return false; } // Valid for client auth? $cert = openssl_x509_read($_SERVER[$this->_params['certificate_field']]); - if (!$this->_params['ignore_purpose'] && - !openssl_x509_checkpurpose($cert, X509_PURPOSE_SSL_CLIENT) && - !openssl_x509_checkpurpose($cert, X509_PURPOSE_ANY)) { + if (!$this->_params['ignore_purpose'] + && !openssl_x509_checkpurpose($cert, X509_PURPOSE_SSL_CLIENT) + && !openssl_x509_checkpurpose($cert, X509_PURPOSE_ANY)) { return false; } diff --git a/migration/Horde/Auth/1_horde_auth_base_tables.php b/migration/Horde/Auth/1_horde_auth_base_tables.php index 36b411f..5e18280 100644 --- a/migration/Horde/Auth/1_horde_auth_base_tables.php +++ b/migration/Horde/Auth/1_horde_auth_base_tables.php @@ -1,4 +1,5 @@ tables())) { - $t = $this->createTable('horde_users', array('autoincrementKey' => array('user_uid'))); - $t->column('user_uid', 'string', array('limit' => 255, 'null' => false)); - $t->column('user_pass', 'string', array('limit' => 255, 'null' => false)); + $t = $this->createTable('horde_users', ['autoincrementKey' => ['user_uid']]); + $t->column('user_uid', 'string', ['limit' => 255, 'null' => false]); + $t->column('user_pass', 'string', ['limit' => 255, 'null' => false]); $t->column('user_soft_expiration_date', 'integer'); $t->column('user_hard_expiration_date', 'integer'); $t->end(); diff --git a/phpunit.xml.dist b/phpunit.xml.dist index bd9744f..b1d94a5 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -16,11 +16,17 @@ test/unit + + test/integration-db + + + test/integration-kolab + - src + lib diff --git a/test/BaseTestCase.php b/test/BaseTestCase.php index a184043..d0f2439 100644 --- a/test/BaseTestCase.php +++ b/test/BaseTestCase.php @@ -3,7 +3,7 @@ /** * Base for testing. * - * Copyright 2010-2017 Horde LLC (http://www.horde.org/) + * Copyright 2010-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. @@ -18,13 +18,12 @@ namespace Horde\Auth\Test; use PHPUnit\Framework\TestCase; - use PHPUnit\Framework\Attributes\CoversNothing; #[CoversNothing] class BaseTestCase extends TestCase { - public static function getCredentials() + public static function getSaltData(): array { return [ ['aprmd5', '$apr1$11CBbKXP$AvvMGBjr81bC/NSMZIxrG.', '11CBbKXP'], @@ -39,17 +38,49 @@ public static function getCredentials() ['plain', 'foobar', ''], ['sha', 'iEPX+SQWIR3p67lj/0zigSWTKHg=', ''], ['smd5', 'ISCNJwzwP30CadahjpkbL2l6bHJxd2h2', 'izlrqwhv'], - ['smd5', '{SMD5}ISCNJwzwP30CadahjpkbL2l6bHJxd2h2', 'izlrqwhv', true], ['smd5', 'bn3EnZ0TFc+yyx3KotqS5GlydmM=', 'irvc'], ['smd5', 'GZ4KWKk2W6eSOHjVXLhOOzADuwA=', hex2bin('3003bb00')], ['smd5', '6y2n+CGCZhuB32dyFu3keQtY0Vc=', hex2bin('0b58d157')], ['ssha', 'buQrQ9vazjrHtO6oIfSZhSBjVxdjemZvZHVubg==', 'czfodunn'], ['ssha', 'BLDmpxHYTH2/Bmg4veVfbglU68jQKEuK', hex2bin('d0284b8a')], ['ssha', '2iXr83rPabLxmrx7uulT4W7mJFrawT41', hex2bin('dac13e35')], - ['ssha', '{SSHA}6IeOcols85dHJeTHevA356ruftrA2PRX', hex2bin('c0d8f457'), true], ['ssha', '6IeOcols85dHJeTHevA356ruftrA2PRX', hex2bin('c0d8f457')], - ['ssha256', '{SSHA256}wnD9GBo+WIXZ+bVD7DjoDokQBjkVgtufXyBh1EqfXn11+sUG', hex2bin('75fac506'), true], ['ssha256', 'wnD9GBo+WIXZ+bVD7DjoDokQBjkVgtufXyBh1EqfXn11+sUG', hex2bin('75fac506')], ]; } + + public static function getCryptedData(): array + { + return [ + ['aprmd5', '$apr1$11CBbKXP$AvvMGBjr81bC/NSMZIxrG.', '11CBbKXP', false], + ['crypt', '8e3IWstJmsmxs', '8e', false], + ['crypt-blowfish', '$2a$08$xJOYeQ7koDohq69yZeFXNO5TvrTlFLs4NP2..EcZP57oeOVgXoxqy', '$2a$08$xJOYeQ7koDohq69yZeFXNO$', false], + ['crypt-des', '45MibW6/G3XEY', '45', false], + ['crypt-md5', '$1$537a3a0e$CWyLVJdQKfxbKPBv/Efzm0', '$1$537a3a0e$', false], + ['md5-base64', 'OFj2IjCsPJFfMAxmQxLGPw==', '', false], + ['md5-hex', '3858f62230ac3c915f300c664312c63f', '', false], + ['msad', "\"\0f\0o\0o\0b\0a\0r\0\"\0", '', false], + ['mysql', '*9B500343BC52E2911172EB52AE5CF4847604C6E5', '', false], + ['plain', 'foobar', '', false], + ['sha', 'iEPX+SQWIR3p67lj/0zigSWTKHg=', '', false], + ['smd5', 'ISCNJwzwP30CadahjpkbL2l6bHJxd2h2', 'izlrqwhv', false], + ['smd5', '{SMD5}ISCNJwzwP30CadahjpkbL2l6bHJxd2h2', 'izlrqwhv', true], + ['smd5', 'bn3EnZ0TFc+yyx3KotqS5GlydmM=', 'irvc', false], + ['smd5', 'GZ4KWKk2W6eSOHjVXLhOOzADuwA=', hex2bin('3003bb00'), false], + ['smd5', '6y2n+CGCZhuB32dyFu3keQtY0Vc=', hex2bin('0b58d157'), false], + ['ssha', 'buQrQ9vazjrHtO6oIfSZhSBjVxdjemZvZHVubg==', 'czfodunn', false], + ['ssha', 'BLDmpxHYTH2/Bmg4veVfbglU68jQKEuK', hex2bin('d0284b8a'), false], + ['ssha', '2iXr83rPabLxmrx7uulT4W7mJFrawT41', hex2bin('dac13e35'), false], + ['ssha', '{SSHA}6IeOcols85dHJeTHevA356ruftrA2PRX', hex2bin('c0d8f457'), true], + ['ssha', '6IeOcols85dHJeTHevA356ruftrA2PRX', hex2bin('c0d8f457'), false], + ['ssha256', '{SSHA256}wnD9GBo+WIXZ+bVD7DjoDokQBjkVgtufXyBh1EqfXn11+sUG', hex2bin('75fac506'), true], + ['ssha256', 'wnD9GBo+WIXZ+bVD7DjoDokQBjkVgtufXyBh1EqfXn11+sUG', hex2bin('75fac506'), false], + ]; + } + + public static function getCredentials(): array + { + // Deprecated: Use getSaltData() or getCryptedData() instead + return self::getCryptedData(); + } } diff --git a/test/sql/Locks.php b/test/Unnamespaced/SqlLockTestBase.php similarity index 56% rename from test/sql/Locks.php rename to test/Unnamespaced/SqlLockTestBase.php index aab2346..b9ab9bf 100644 --- a/test/sql/Locks.php +++ b/test/Unnamespaced/SqlLockTestBase.php @@ -1,25 +1,21 @@ $lockMigrationsPath, 'schemaTableName' => 'horde_lock_schema_info'] ); self::$locksMigrator->up(); - self::$locks = new Horde_Lock_Sql(['db' => self::$db]); + self::$locks = new \Horde_Lock_Sql(['db' => self::$db]); - self::$auth = new Horde_Auth_Sql(['db' => self::$db, + self::$auth = new \Horde_Auth_Sql(['db' => self::$db, 'encryption' => 'plain', 'lock_api' => self::$locks, ]); - } public function setUp(): void { - if (!class_exists('Horde_Db')) { - $this->markTestSkipped('The Horde_Db package is not installed!'); - } - if (!class_exists('Horde_Lock')) { - $this->markTestSkipped('The Horde_Lock package is not installed!'); - } if (self::$skip) { $this->markTestSkipped(self::$skip); } - if (!self::$db) { - $this->markTestSkipped(self::$reason); - } else { - // portability: use DELETE because SQLite has no truncate - $sql = "DELETE FROM horde_locks"; - self::$db->execute($sql); - } + // portability: use DELETE because SQLite has no truncate + $sql = "DELETE FROM horde_locks"; + self::$db->execute($sql); } @@ -86,14 +82,12 @@ public function testAuthenticate() public function testLockUserOnceWorks() { self::$auth->lockUser('konqui'); + $this->assertTrue(self::$auth->isLocked('konqui')); } - /** - * @expectedException Horde_Auth_Exception - */ - public function testLockUserTwiceFails() { + $this->expectException(\Horde_Auth_Exception::class); self::$auth->lockUser('konqui'); self::$auth->lockUser('konqui'); } @@ -120,6 +114,7 @@ public function testUnlockUnlockedDoesNotThrowException() self::$auth->unlockUser('konqui'); self::$auth->unlockUser('konqui'); self::$auth->unlockUser('konqui'); + $this->assertFalse(self::$auth->isLocked('konqui')); } } diff --git a/test/Unnamespaced/SqlTestBase.php b/test/Unnamespaced/SqlTestBase.php new file mode 100644 index 0000000..2e31436 --- /dev/null +++ b/test/Unnamespaced/SqlTestBase.php @@ -0,0 +1,153 @@ + $dir, + 'schemaTableName' => 'horde_auth_schema_info'] + ); + self::$migrator->up(); + + self::$auth = new \Horde_Auth_Sql(['db' => self::$db, 'encryption' => 'plain']); + // Don't rely on auth->addUser as this is the unit under test + $row = "INSERT INTO horde_users VALUES ('mozilla', 'liketokyo', NULL, NULL);"; + self::$db->execute($row); + $row = "INSERT INTO horde_users VALUES ('konqui', 'kde', NULL, NULL);"; + self::$db->execute($row); + $row = "INSERT INTO horde_users VALUES ('tux', 'fish', NULL, NULL);"; + self::$db->execute($row); + } + + public static function tearDownAfterClass(): void + { + if (self::$migrator) { + self::$migrator->down(); + } + if (self::$db) { + self::$db->disconnect(); + self::$db = null; + } + parent::tearDownAfterClass(); + } + + public function setUp(): void + { + // No skip logic - integration tests are opt-in + } + + public function testAuthenticate() + { + if (class_exists('Horde_Db_Adapter_Pdo_Sqlite')) { + $this->assertTrue(self::$auth->authenticate('tux', ['password' => 'fish'])); + } + } + + public function testListUsers() + { + $resultUnsorted = self::$auth->listUsers(); + sort($resultUnsorted); + $this->assertEquals(['konqui', 'mozilla', 'tux'], $resultUnsorted); + } + public function testListUsersWithSorting() + { + $this->assertEquals(['konqui', 'mozilla', 'tux'], self::$auth->listUsers(true)); + } + + public function testLockCapability() + { + $this->assertFalse(self::$auth->hasCapability('lock')); + } + + public function testExistsReturnsTrueForPresentUser() + { + $this->assertTrue(self::$auth->exists('konqui')); + } + + public function testExistsReturnsFalseForMissingUser() + { + $this->assertFalse(self::$auth->exists('beasty')); + } + + public function testAddUserCreatesNewUser() + { + self::$auth->addUser('newuser', ['password' => 'newpass']); + $this->assertTrue(self::$auth->exists('newuser')); + } + + public function testAddedUserCanAuthenticate() + { + self::$auth->addUser('alice', ['password' => 'alicepass']); + $this->assertTrue(self::$auth->authenticate('alice', ['password' => 'alicepass'])); + } + + public function testRemoveUserDeletesUser() + { + self::$auth->addUser('tempuser', ['password' => 'temppass']); + $this->assertTrue(self::$auth->exists('tempuser')); + self::$auth->removeUser('tempuser'); + $this->assertFalse(self::$auth->exists('tempuser')); + } + + public function testUpdateUserChangesUserId() + { + self::$auth->addUser('oldname', ['password' => 'pass123']); + self::$auth->updateUser('oldname', 'newname', ['password' => 'pass123']); + $this->assertFalse(self::$auth->exists('oldname')); + $this->assertTrue(self::$auth->exists('newname')); + } + + public function testUpdateUserChangesPassword() + { + self::$auth->addUser('bob', ['password' => 'oldpass']); + self::$auth->updateUser('bob', 'bob', ['password' => 'newpass']); + $this->assertFalse(self::$auth->authenticate('bob', ['password' => 'oldpass'])); + $this->assertTrue(self::$auth->authenticate('bob', ['password' => 'newpass'])); + } + + public function testResetPasswordReturnsNewPassword() + { + self::$auth->addUser('charlie', ['password' => 'oldpass']); + $newPass = self::$auth->resetPassword('charlie'); + $this->assertIsString($newPass); + $this->assertGreaterThan(0, strlen($newPass)); + } + + public function testResetPasswordChangesPassword() + { + self::$auth->addUser('david', ['password' => 'oldpass']); + $newPass = self::$auth->resetPassword('david'); + $this->assertFalse(self::$auth->authenticate('david', ['password' => 'oldpass'])); + $this->assertTrue(self::$auth->authenticate('david', ['password' => $newPass])); + } +} diff --git a/test/integration-db/SqliteLockTest.php b/test/integration-db/SqliteLockTest.php new file mode 100644 index 0000000..ffe8051 --- /dev/null +++ b/test/integration-db/SqliteLockTest.php @@ -0,0 +1,30 @@ + ':memory:', + 'charset' => 'utf-8', + ]); + parent::setUpBeforeClass(); + } +} diff --git a/test/integration-db/SqliteTest.php b/test/integration-db/SqliteTest.php new file mode 100644 index 0000000..bbb9c22 --- /dev/null +++ b/test/integration-db/SqliteTest.php @@ -0,0 +1,30 @@ + ':memory:', + 'charset' => 'utf-8', + ]); + parent::setUpBeforeClass(); + } +} diff --git a/test/kolab/KolabTest.php b/test/integration-kolab/KolabTest.php similarity index 85% rename from test/kolab/KolabTest.php rename to test/integration-kolab/KolabTest.php index 7d17e4c..dd2dd2d 100644 --- a/test/kolab/KolabTest.php +++ b/test/integration-kolab/KolabTest.php @@ -3,7 +3,7 @@ /** * Test the Horde_Auth_Kolab:: class. * - * Copyright 2010-2017 Horde LLC (http://www.horde.org/) + * Copyright 2010-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. @@ -15,21 +15,28 @@ * @license http://www.horde.org/licenses/lgpl21 LGPL-2.1 */ -namespace Horde\Auth\Test\Unit; +namespace Horde\Auth\Test\IntegrationKolab; use Horde\Auth\Test\BaseTestCase; +use Horde_Auth_Kolab; +use Horde_Auth_Exception; +use Horde_Auth; +use Horde_Kolab_Session_Exception_Badlogin; +use Horde_Kolab_Session_Exception; +use PHPUnit\Framework\Attributes\CoversClass; -use PHPUnit\Framework\Attributes\CoversNothing; - -#[CoversNothing] +#[CoversClass(Horde_Auth_Kolab::class)] class KolabTest extends BaseTestCase { + private $kolab; + private Horde_Auth_Kolab $driver; + public function setUp(): void { if (!interface_exists('Horde_Kolab_Session')) { $this->markTestSkipped('The Kolab_Session package is apparently not installed (Interface Horde_Kolab_Session is unavailable).'); } - $this->kolab = $this->getMock('Horde_Kolab_Session'); + $this->kolab = $this->createMock('Horde_Kolab_Session'); $this->driver = new Horde_Auth_Kolab(['kolab' => $this->kolab]); } diff --git a/test/sql/Base.php b/test/sql/Base.php deleted file mode 100644 index d6995d1..0000000 --- a/test/sql/Base.php +++ /dev/null @@ -1,107 +0,0 @@ - $dir, - 'schemaTableName' => 'horde_auth_schema_info'] - ); - self::$migrator->up(); - - self::$auth = new Horde_Auth_Sql(['db' => self::$db, 'encryption' => 'plain']); - // Don't rely on auth->addUser as this is the unit under test - $row = "INSERT INTO horde_users VALUES ('mozilla', 'liketokyo', NULL, NULL);"; - self::$db->execute($row); - $row = "INSERT INTO horde_users VALUES ('konqui', 'kde', NULL, NULL);"; - self::$db->execute($row); - $row = "INSERT INTO horde_users VALUES ('tux', 'fish', NULL, NULL);"; - self::$db->execute($row); - } - - public static function tearDownAfterClass(): void - { - if (self::$migrator) { - self::$migrator->down(); - } - if (self::$db) { - self::$db->disconnect(); - self::$db = null; - } - parent::tearDownAfterClass(); - } - - public function setUp(): void - { - if (!self::$db) { - $this->markTestSkipped(self::$reason); - } - } - - public function testAuthenticate() - { - if (class_exists('Horde_Db_Adapter_Pdo_Sqlite')) { - $this->assertTrue(self::$auth->authenticate('tux', ['password' => 'fish'])); - } - } - - public function testListUsers() - { - $resultUnsorted = self::$auth->listUsers(); - sort($resultUnsorted); - $this->assertEquals(['konqui', 'mozilla', 'tux'], $resultUnsorted); - } - public function testListUsersWithSorting() - { - $this->assertEquals(['konqui', 'mozilla', 'tux'], self::$auth->listUsers(true)); - } - - public function testLockCapability() - { - $this->assertFalse(self::$auth->hasCapability('lock')); - } - - public function testExistsReturnsTrueForPresentUser() - { - $this->assertTrue(self::$auth->exists('konqui')); - } - - public function testExistsReturnsFalseForMissingUser() - { - $this->assertFalse(self::$auth->exists('beasty')); - } -} diff --git a/test/sql/Pdo/SqliteLockTest.php b/test/sql/Pdo/SqliteLockTest.php deleted file mode 100644 index 5022622..0000000 --- a/test/sql/Pdo/SqliteLockTest.php +++ /dev/null @@ -1,30 +0,0 @@ -create(); - parent::setUpBeforeClass(); - } catch (Horde_Test_Exception $e) { - self::$reason = 'Sqlite not available.'; - } - } - -} diff --git a/test/sql/Pdo/SqliteTest.php b/test/sql/Pdo/SqliteTest.php deleted file mode 100644 index ddab693..0000000 --- a/test/sql/Pdo/SqliteTest.php +++ /dev/null @@ -1,31 +0,0 @@ -create(); - parent::setUpBeforeClass(); - } catch (Horde_Test_Exception $e) { - self::$reason = 'Sqlite not available.'; - } - } - -} diff --git a/test/unit/AuthTest.php b/test/unit/AuthTest.php index 4cbcf02..84b840b 100644 --- a/test/unit/AuthTest.php +++ b/test/unit/AuthTest.php @@ -19,20 +19,19 @@ use Horde\Auth\Test\BaseTestCase; use Horde_Auth; - -use PHPUnit\Framework\Attributes\CoversNothing; +use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\DataProvider; -#[CoversNothing] +#[CoversClass(Horde_Auth::class)] class AuthTest extends BaseTestCase { - #[DataProvider('getCredentials')] + #[DataProvider('getSaltData')] public function testGetSalt($encryption, $password, $salt) { $this->assertEquals($salt, Horde_Auth::getSalt($encryption, $password, 'foobar')); } - #[DataProvider('getCredentials')] + #[DataProvider('getCryptedData')] public function testGetCryptedPassword($encryption, $password, $salt, $show_encryption = false) { $this->assertEquals($password, Horde_Auth::getCryptedPassword('foobar', $password, $encryption, $show_encryption)); diff --git a/test/unit/AutoTest.php b/test/unit/AutoTest.php new file mode 100644 index 0000000..6204c3e --- /dev/null +++ b/test/unit/AutoTest.php @@ -0,0 +1,125 @@ +assertInstanceOf(Horde_Auth_Auto::class, $driver); + } + + public function testConstructorAcceptsCustomUsername() + { + $driver = new Horde_Auth_Auto(['username' => 'custom_user']); + $this->assertInstanceOf(Horde_Auth_Auto::class, $driver); + } + + public function testConstructorAcceptsPasswordParameter() + { + $driver = new Horde_Auth_Auto(['password' => 'secret123']); + $this->assertInstanceOf(Horde_Auth_Auto::class, $driver); + } + + public function testConstructorAcceptsRequestUserParameter() + { + $driver = new Horde_Auth_Auto(['requestuser' => true]); + $this->assertInstanceOf(Horde_Auth_Auto::class, $driver); + } + + public function testTransparentCapabilityEnabled() + { + $driver = new Horde_Auth_Auto(); + $this->assertTrue($driver->hasCapability('transparent')); + } + + public function testTransparentReturnsTrue() + { + $driver = new Horde_Auth_Auto(['username' => 'test_auto_user']); + $this->assertTrue($driver->transparent()); + } + + public function testTransparentUsesDefaultUsername() + { + $driver = new Horde_Auth_Auto(['username' => 'my_default_user']); + $driver->transparent(); + $this->assertEquals('my_default_user', $driver->getCredential('userId')); + } + + public function testTransparentUsesRequestUsernameWhenEnabled() + { + $_REQUEST['username'] = 'request_user'; + $driver = new Horde_Auth_Auto([ + 'username' => 'default_user', + 'requestuser' => true, + ]); + $driver->transparent(); + $this->assertEquals('request_user', $driver->getCredential('userId')); + unset($_REQUEST['username']); + } + + public function testTransparentIgnoresRequestUsernameWhenDisabled() + { + $_REQUEST['username'] = 'request_user'; + $driver = new Horde_Auth_Auto([ + 'username' => 'default_user', + 'requestuser' => false, + ]); + $driver->transparent(); + $this->assertEquals('default_user', $driver->getCredential('userId')); + unset($_REQUEST['username']); + } + + public function testTransparentSetsPasswordCredential() + { + $driver = new Horde_Auth_Auto([ + 'username' => 'test_user', + 'password' => 'test_password', + ]); + $driver->transparent(); + $creds = $driver->getCredential('credentials'); + $this->assertEquals('test_password', $creds['password']); + } + + public function testAuthenticateThrowsException() + { + $driver = new Horde_Auth_Auto(); + $this->assertFalse($driver->authenticate('anyuser', ['password' => 'anypass'])); + } + + /** + * Tests that _authenticate() throws "Unsupported" exception. + * The archaic Horde_Auth design treats _authenticate() as the canonical driver behavior, + * with authenticate() wrapping exceptions to booleans. Testing _authenticate() verifies error codes. + */ + public function testProtectedAuthenticateThrowsUnsupportedException() + { + $driver = new Horde_Auth_Auto(); + $method = new \ReflectionMethod($driver, '_authenticate'); + $method->setAccessible(true); + + $this->expectException(Horde_Auth_Exception::class); + $this->expectExceptionMessage('Unsupported'); + $method->invoke($driver, 'anyuser', ['password' => 'anypass']); + } +} diff --git a/test/unit/CompositeTest.php b/test/unit/CompositeTest.php new file mode 100644 index 0000000..3c710f3 --- /dev/null +++ b/test/unit/CompositeTest.php @@ -0,0 +1,240 @@ +authDriver = $this->createMock(Horde_Auth_Base::class); + $this->adminDriver = $this->createMock(Horde_Auth_Base::class); + } + + public function testConstructorRequiresAdminDriver() + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Missing admin_driver parameter'); + new Horde_Auth_Composite([]); + } + + public function testConstructorRequiresAuthDriver() + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Missing auth_driver parameter'); + new Horde_Auth_Composite(['admin_driver' => $this->adminDriver]); + } + + public function testConstructorAcceptsBothDrivers() + { + $driver = new Horde_Auth_Composite([ + 'auth_driver' => $this->authDriver, + 'admin_driver' => $this->adminDriver, + ]); + $this->assertInstanceOf(Horde_Auth_Composite::class, $driver); + } + + public function testAuthenticateDelegatesToAuthDriver() + { + $this->authDriver->expects($this->once()) + ->method('authenticate') + ->with('testuser', ['password' => 'testpass']) + ->willReturn(true); + + $driver = new Horde_Auth_Composite([ + 'auth_driver' => $this->authDriver, + 'admin_driver' => $this->adminDriver, + ]); + + $this->assertTrue($driver->authenticate('testuser', ['password' => 'testpass'])); + } + + public function testAuthenticateReturnsFalseWhenAuthDriverFails() + { + $this->authDriver->expects($this->once()) + ->method('authenticate') + ->with('testuser', ['password' => 'wrongpass']) + ->willReturn(false); + + $this->authDriver->expects($this->atLeastOnce()) + ->method('getError') + ->willReturnOnConsecutiveCalls('Bad login', 1); + + $driver = new Horde_Auth_Composite([ + 'auth_driver' => $this->authDriver, + 'admin_driver' => $this->adminDriver, + ]); + + $this->assertFalse($driver->authenticate('testuser', ['password' => 'wrongpass'])); + } + + /** + * Tests that _authenticate() properly delegates and throws when auth_driver fails. + * The archaic Horde_Auth design treats _authenticate() as the canonical driver behavior, + * with authenticate() wrapping exceptions to booleans. Testing _authenticate() verifies error codes. + */ + public function testProtectedAuthenticateThrowsExceptionWhenAuthDriverFails() + { + $this->authDriver->expects($this->once()) + ->method('authenticate') + ->with('testuser', ['password' => 'wrongpass']) + ->willReturn(false); + + $this->authDriver->expects($this->atLeastOnce()) + ->method('getError') + ->willReturnOnConsecutiveCalls('Bad login', 1); + + $driver = new Horde_Auth_Composite([ + 'auth_driver' => $this->authDriver, + 'admin_driver' => $this->adminDriver, + ]); + + $method = new \ReflectionMethod($driver, '_authenticate'); + $method->setAccessible(true); + + $this->expectException(Horde_Auth_Exception::class); + $method->invoke($driver, 'testuser', ['password' => 'wrongpass']); + } + + public function testTransparentDelegatesToAuthDriver() + { + $this->authDriver->expects($this->once()) + ->method('transparent') + ->willReturn(true); + + $driver = new Horde_Auth_Composite([ + 'auth_driver' => $this->authDriver, + 'admin_driver' => $this->adminDriver, + ]); + + $this->assertTrue($driver->transparent()); + } + + public function testHasCapabilityDelegatesToAdminDriver() + { + $this->adminDriver->expects($this->once()) + ->method('hasCapability') + ->with('add') + ->willReturn(true); + + $driver = new Horde_Auth_Composite([ + 'auth_driver' => $this->authDriver, + 'admin_driver' => $this->adminDriver, + ]); + + $this->assertTrue($driver->hasCapability('add')); + } + + public function testAddUserDelegatesToAdminDriver() + { + $this->adminDriver->expects($this->once()) + ->method('addUser') + ->with('newuser', ['password' => 'newpass']); + + $driver = new Horde_Auth_Composite([ + 'auth_driver' => $this->authDriver, + 'admin_driver' => $this->adminDriver, + ]); + + $driver->addUser('newuser', ['password' => 'newpass']); + $this->assertTrue(true); + } + + public function testUpdateUserDelegatesToAdminDriver() + { + $this->adminDriver->expects($this->once()) + ->method('updateUser') + ->with('olduser', 'newuser', ['password' => 'newpass']); + + $driver = new Horde_Auth_Composite([ + 'auth_driver' => $this->authDriver, + 'admin_driver' => $this->adminDriver, + ]); + + $driver->updateUser('olduser', 'newuser', ['password' => 'newpass']); + $this->assertTrue(true); + } + + public function testRemoveUserDelegatesToAdminDriver() + { + $this->adminDriver->expects($this->once()) + ->method('removeUser') + ->with('testuser'); + + $driver = new Horde_Auth_Composite([ + 'auth_driver' => $this->authDriver, + 'admin_driver' => $this->adminDriver, + ]); + + $driver->removeUser('testuser'); + $this->assertTrue(true); + } + + public function testResetPasswordDelegatesToAdminDriver() + { + $this->adminDriver->expects($this->once()) + ->method('resetPassword') + ->with('testuser') + ->willReturn('newpass123'); + + $driver = new Horde_Auth_Composite([ + 'auth_driver' => $this->authDriver, + 'admin_driver' => $this->adminDriver, + ]); + + $this->assertEquals('newpass123', $driver->resetPassword('testuser')); + } + + public function testListUsersDelegatesToAdminDriver() + { + $this->adminDriver->expects($this->once()) + ->method('listUsers') + ->with(false) + ->willReturn(['user1', 'user2']); + + $driver = new Horde_Auth_Composite([ + 'auth_driver' => $this->authDriver, + 'admin_driver' => $this->adminDriver, + ]); + + $this->assertEquals(['user1', 'user2'], $driver->listUsers()); + } + + public function testExistsDelegatesToAdminDriver() + { + $this->adminDriver->expects($this->once()) + ->method('exists') + ->with('testuser') + ->willReturn(true); + + $driver = new Horde_Auth_Composite([ + 'auth_driver' => $this->authDriver, + 'admin_driver' => $this->adminDriver, + ]); + + $this->assertTrue($driver->exists('testuser')); + } +} diff --git a/test/unit/LdapTest.php b/test/unit/LdapTest.php new file mode 100644 index 0000000..be66063 --- /dev/null +++ b/test/unit/LdapTest.php @@ -0,0 +1,337 @@ +ldap = $this->createMock(Horde_Ldap::class); + } + + public function testConstructorRequiresBaseDn() + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Missing basedn parameter'); + new Horde_Auth_Ldap([]); + } + + public function testConstructorRequiresLdapConnection() + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Missing ldap parameter'); + new Horde_Auth_Ldap(['basedn' => 'dc=example,dc=com', 'uid' => 'uid']); + } + + public function testConstructorRequiresUid() + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Missing uid parameter'); + new Horde_Auth_Ldap([ + 'basedn' => 'dc=example,dc=com', + 'ldap' => $this->ldap, + ]); + } + + public function testConstructorStoresLdapConnection() + { + $driver = new Horde_Auth_Ldap([ + 'basedn' => 'dc=example,dc=com', + 'ldap' => $this->ldap, + 'uid' => 'uid', + ]); + $this->assertInstanceOf(Horde_Auth_Ldap::class, $driver); + } + + public function testAuthenticateWithEmptyPasswordReturnsFalse() + { + $this->driver = new Horde_Auth_Ldap([ + 'basedn' => 'dc=example,dc=com', + 'ldap' => $this->ldap, + 'uid' => 'uid', + 'password_expiration' => 'no', + ]); + + $this->assertFalse($this->driver->authenticate('testuser', ['password' => ''])); + } + + /** + * Tests that _authenticate() throws proper exception codes for error handling. + * The archaic Horde_Auth design treats _authenticate() as the canonical driver behavior, + * with authenticate() wrapping exceptions to booleans. Testing _authenticate() verifies error codes. + */ + public function testProtectedAuthenticateThrowsExceptionOnEmptyPassword() + { + $this->driver = new Horde_Auth_Ldap([ + 'basedn' => 'dc=example,dc=com', + 'ldap' => $this->ldap, + 'uid' => 'uid', + 'password_expiration' => 'no', + ]); + + $method = new \ReflectionMethod($this->driver, '_authenticate'); + $method->setAccessible(true); + + $this->expectException(Horde_Auth_Exception::class); + $method->invoke($this->driver, 'testuser', ['password' => '']); + } + + /** + * Tests that _authenticate() throws proper exception with correct error code. + * The archaic Horde_Auth design treats _authenticate() as the canonical driver behavior, + * with authenticate() wrapping exceptions to booleans. Testing _authenticate() verifies error codes. + */ + public function testProtectedAuthenticateThrowsExceptionWhenUserNotFound() + { + $this->ldap->expects($this->once()) + ->method('bind') + ->willReturn(true); + + $this->ldap->expects($this->once()) + ->method('findUserDN') + ->with('testuser') + ->willThrowException(new Horde_Exception_NotFound()); + + $this->driver = new Horde_Auth_Ldap([ + 'basedn' => 'dc=example,dc=com', + 'ldap' => $this->ldap, + 'uid' => 'uid', + 'password_expiration' => 'no', + ]); + + $method = new \ReflectionMethod($this->driver, '_authenticate'); + $method->setAccessible(true); + + $this->expectException(Horde_Auth_Exception::class); + $method->invoke($this->driver, 'testuser', ['password' => 'testpass']); + } + + public function testAuthenticateBindsAndFindsUserDn() + { + $this->ldap->expects($this->atLeastOnce()) + ->method('bind') + ->willReturn(true); + + $this->ldap->expects($this->once()) + ->method('findUserDN') + ->with('testuser') + ->willReturn('uid=testuser,dc=example,dc=com'); + + $this->driver = new Horde_Auth_Ldap([ + 'basedn' => 'dc=example,dc=com', + 'ldap' => $this->ldap, + 'uid' => 'uid', + 'password_expiration' => 'no', + ]); + + $this->driver->authenticate('testuser', ['password' => 'testpass']); + $this->assertTrue(true); + } + + public function testAuthenticateReturnsFalseWhenUserNotFound() + { + $this->ldap->expects($this->once()) + ->method('bind') + ->willReturn(true); + + $this->ldap->expects($this->once()) + ->method('findUserDN') + ->with('testuser') + ->willThrowException(new Horde_Exception_NotFound()); + + $this->driver = new Horde_Auth_Ldap([ + 'basedn' => 'dc=example,dc=com', + 'ldap' => $this->ldap, + 'uid' => 'uid', + 'password_expiration' => 'no', + ]); + + $this->assertFalse($this->driver->authenticate('testuser', ['password' => 'testpass'])); + } + + public function testListUsersReturnsUserArray() + { + $entry1 = $this->createMock(Horde_Ldap_Entry::class); + $entry1->expects($this->once())->method('exists')->with('uid')->willReturn(true); + $entry1->expects($this->once())->method('getValue')->with('uid', 'single')->willReturn('user1'); + + $entry2 = $this->createMock(Horde_Ldap_Entry::class); + $entry2->expects($this->once())->method('exists')->with('uid')->willReturn(true); + $entry2->expects($this->once())->method('getValue')->with('uid', 'single')->willReturn('user2'); + + $searchResult = new \ArrayIterator([$entry1, $entry2]); + + $this->ldap->expects($this->once()) + ->method('search') + ->willReturn($searchResult); + + $this->driver = new Horde_Auth_Ldap([ + 'basedn' => 'dc=example,dc=com', + 'ldap' => $this->ldap, + 'uid' => 'uid', + 'filter' => '(objectClass=person)', + 'scope' => 'sub', + ]); + + $users = $this->driver->listUsers(); + $this->assertEquals(['user1', 'user2'], $users); + } + + public function testExistsReturnsTrueForPresentUser() + { + $searchResult = $this->createMock(Horde_Ldap_Search::class); + $searchResult->expects($this->atLeastOnce()) + ->method('count') + ->willReturn(1); + + $this->ldap->expects($this->once()) + ->method('search') + ->willReturn($searchResult); + + $this->driver = new Horde_Auth_Ldap([ + 'basedn' => 'dc=example,dc=com', + 'ldap' => $this->ldap, + 'uid' => 'uid', + 'filter' => '(objectClass=person)', + 'scope' => 'sub', + ]); + + $this->assertTrue($this->driver->exists('testuser')); + } + + public function testExistsReturnsFalseForMissingUser() + { + $searchResult = $this->createMock(Horde_Ldap_Search::class); + $searchResult->expects($this->once()) + ->method('count') + ->willReturn(0); + + $this->ldap->expects($this->once()) + ->method('search') + ->willReturn($searchResult); + + $this->driver = new Horde_Auth_Ldap([ + 'basedn' => 'dc=example,dc=com', + 'ldap' => $this->ldap, + 'uid' => 'uid', + 'filter' => '(objectClass=person)', + 'scope' => 'sub', + ]); + + $this->assertFalse($this->driver->exists('nonexistent')); + } + + public function testAddUserThrowsExceptionForActiveDirectory() + { + $this->driver = new Horde_Auth_Ldap([ + 'basedn' => 'dc=example,dc=com', + 'ldap' => $this->ldap, + 'uid' => 'uid', + 'ad' => true, + ]); + + $this->expectException(Horde_Auth_Exception::class); + $this->expectExceptionMessage('Adding users is not supported for Active Directory'); + $this->driver->addUser('testuser', ['password' => 'testpass']); + } + + public function testRemoveUserThrowsExceptionForActiveDirectory() + { + $this->driver = new Horde_Auth_Ldap([ + 'basedn' => 'dc=example,dc=com', + 'ldap' => $this->ldap, + 'uid' => 'uid', + 'ad' => true, + ]); + + $this->expectException(Horde_Auth_Exception::class); + $this->expectExceptionMessage('Removing users is not supported for Active Directory'); + $this->driver->removeUser('testuser'); + } + + public function testUpdateUserThrowsExceptionForActiveDirectory() + { + $this->driver = new Horde_Auth_Ldap([ + 'basedn' => 'dc=example,dc=com', + 'ldap' => $this->ldap, + 'uid' => 'uid', + 'ad' => true, + ]); + + $this->expectException(Horde_Auth_Exception::class); + $this->expectExceptionMessage('Updating users is not supported for Active Directory'); + $this->driver->updateUser('old', 'new', ['password' => 'pass']); + } + + public function testResetPasswordThrowsExceptionForActiveDirectory() + { + $this->driver = new Horde_Auth_Ldap([ + 'basedn' => 'dc=example,dc=com', + 'ldap' => $this->ldap, + 'uid' => 'uid', + 'ad' => true, + ]); + + $this->expectException(Horde_Auth_Exception::class); + $this->expectExceptionMessage('Updating users is not supported for Active Directory'); + $this->driver->resetPassword('testuser'); + } + + public function testActiveDirectoryDisablesResetPasswordCapability() + { + $this->driver = new Horde_Auth_Ldap([ + 'basedn' => 'dc=example,dc=com', + 'ldap' => $this->ldap, + 'uid' => 'uid', + 'ad' => true, + ]); + + $this->assertFalse($this->driver->hasCapability('resetpassword')); + } + + public function testNonActiveDirectoryEnablesAllCapabilities() + { + $this->driver = new Horde_Auth_Ldap([ + 'basedn' => 'dc=example,dc=com', + 'ldap' => $this->ldap, + 'uid' => 'uid', + ]); + + $this->assertTrue($this->driver->hasCapability('add')); + $this->assertTrue($this->driver->hasCapability('update')); + $this->assertTrue($this->driver->hasCapability('resetpassword')); + $this->assertTrue($this->driver->hasCapability('remove')); + $this->assertTrue($this->driver->hasCapability('list')); + $this->assertTrue($this->driver->hasCapability('authenticate')); + } +} diff --git a/test/unit/PamTest.php b/test/unit/PamTest.php new file mode 100644 index 0000000..d6f12e3 --- /dev/null +++ b/test/unit/PamTest.php @@ -0,0 +1,78 @@ +markTestSkipped('PAM extension is loaded, cannot test missing extension behavior'); + } + + $this->expectException(Horde_Auth_Exception::class); + $this->expectExceptionMessage('PAM authentication is not available'); + new Horde_Auth_Pam(); + } + + #[RequiresPhpExtension('pam')] + public function testConstructorSucceedsWithPamExtension() + { + $driver = new Horde_Auth_Pam(); + $this->assertInstanceOf(Horde_Auth_Pam::class, $driver); + } + + #[RequiresPhpExtension('pam')] + public function testConstructorAcceptsServiceParameter() + { + $driver = new Horde_Auth_Pam(['service' => 'myservice']); + $this->assertInstanceOf(Horde_Auth_Pam::class, $driver); + $this->assertEquals('myservice', ini_get('pam.servicename')); + } + + #[RequiresPhpExtension('pam')] + public function testAuthenticateReturnsFalseOnEmptyPassword() + { + $driver = new Horde_Auth_Pam(); + $this->assertFalse($driver->authenticate('testuser', ['password' => ''])); + } + + /** + * Tests that _authenticate() throws proper exception for error handling. + * The archaic Horde_Auth design treats _authenticate() as the canonical driver behavior, + * with authenticate() wrapping exceptions to booleans. Testing _authenticate() verifies error codes. + */ + public function testProtectedAuthenticateThrowsExceptionOnEmptyPassword() + { + if (!extension_loaded('pam')) { + $this->markTestSkipped('PAM extension not available'); + } + + $driver = new Horde_Auth_Pam(); + $method = new \ReflectionMethod($driver, '_authenticate'); + $method->setAccessible(true); + + $this->expectException(Horde_Auth_Exception::class); + $method->invoke($driver, 'testuser', ['password' => '']); + } +} diff --git a/test/unit/PasswdTest.php b/test/unit/PasswdTest.php index 8fb6f83..4d295b5 100644 --- a/test/unit/PasswdTest.php +++ b/test/unit/PasswdTest.php @@ -1,10 +1,9 @@ driver = new Horde_Auth_Passwd( diff --git a/test/unit/SqlTest.php b/test/unit/SqlTest.php new file mode 100644 index 0000000..8487535 --- /dev/null +++ b/test/unit/SqlTest.php @@ -0,0 +1,199 @@ +db = $this->createMock(Horde_Db_Adapter::class); + } + + public function testConstructorRequiresDatabaseAdapter() + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Missing db parameter'); + new Horde_Auth_Sql([]); + } + + public function testConstructorAcceptsDatabaseAdapter() + { + $driver = new Horde_Auth_Sql(['db' => $this->db]); + $this->assertInstanceOf(Horde_Auth_Sql::class, $driver); + } + + public function testConstructorSetsDefaultEncryption() + { + $driver = new Horde_Auth_Sql(['db' => $this->db]); + $this->assertTrue($driver->hasCapability('authenticate')); + } + + public function testConstructorAcceptsCustomTableName() + { + $driver = new Horde_Auth_Sql([ + 'db' => $this->db, + 'table' => 'custom_users', + ]); + $this->assertInstanceOf(Horde_Auth_Sql::class, $driver); + } + + public function testConstructorAcceptsCustomFieldNames() + { + $driver = new Horde_Auth_Sql([ + 'db' => $this->db, + 'username_field' => 'login', + 'password_field' => 'passwd', + ]); + $this->assertInstanceOf(Horde_Auth_Sql::class, $driver); + } + + public function testHasAllCrudCapabilities() + { + $driver = new Horde_Auth_Sql(['db' => $this->db]); + $this->assertTrue($driver->hasCapability('add')); + $this->assertTrue($driver->hasCapability('update')); + $this->assertTrue($driver->hasCapability('remove')); + $this->assertTrue($driver->hasCapability('resetpassword')); + $this->assertTrue($driver->hasCapability('list')); + $this->assertTrue($driver->hasCapability('authenticate')); + } + + public function testListUsersQueriesDatabase() + { + $this->db->expects($this->once()) + ->method('selectValues') + ->with('SELECT user_uid FROM horde_users ORDER BY user_uid ASC') + ->willReturn(['user1', 'user2', 'user3']); + + $driver = new Horde_Auth_Sql(['db' => $this->db]); + $users = $driver->listUsers(true); + $this->assertEquals(['user1', 'user2', 'user3'], $users); + } + + public function testListUsersWithoutSortingQueriesWithoutOrderBy() + { + $this->db->expects($this->once()) + ->method('selectValues') + ->with('SELECT user_uid FROM horde_users') + ->willReturn(['user3', 'user1', 'user2']); + + $driver = new Horde_Auth_Sql(['db' => $this->db]); + $users = $driver->listUsers(false); + $this->assertEquals(['user3', 'user1', 'user2'], $users); + } + + public function testExistsReturnsTrueWhenUserFound() + { + $this->db->expects($this->once()) + ->method('selectValue') + ->willReturn('testuser'); + + $driver = new Horde_Auth_Sql(['db' => $this->db]); + $this->assertTrue($driver->exists('testuser')); + } + + public function testExistsReturnsFalseWhenUserNotFound() + { + $this->db->expects($this->once()) + ->method('selectValue') + ->willReturn(null); + + $driver = new Horde_Auth_Sql(['db' => $this->db]); + $this->assertFalse($driver->exists('nonexistent')); + } + + /** + * Tests that _authenticate() queries the database and validates password. + * The archaic Horde_Auth design treats _authenticate() as the canonical driver behavior, + * with authenticate() wrapping exceptions to booleans. Testing _authenticate() verifies error codes. + */ + public function testProtectedAuthenticateQueriesDatabase() + { + $this->db->expects($this->once()) + ->method('selectOne') + ->willReturn([ + 'user_uid' => 'testuser', + 'user_pass' => md5('testpass'), + ]); + + $driver = new Horde_Auth_Sql([ + 'db' => $this->db, + 'encryption' => 'md5-hex', + ]); + + $method = new \ReflectionMethod($driver, '_authenticate'); + $method->setAccessible(true); + $method->invoke($driver, 'testuser', ['password' => 'testpass']); + $this->assertTrue(true); + } + + /** + * Tests that _authenticate() throws exception on wrong password. + * The archaic Horde_Auth design treats _authenticate() as the canonical driver behavior, + * with authenticate() wrapping exceptions to booleans. Testing _authenticate() verifies error codes. + */ + public function testProtectedAuthenticateThrowsExceptionOnWrongPassword() + { + $this->db->expects($this->once()) + ->method('selectOne') + ->willReturn([ + 'user_uid' => 'testuser', + 'user_pass' => md5('correctpass'), + ]); + + $driver = new Horde_Auth_Sql([ + 'db' => $this->db, + 'encryption' => 'md5-hex', + ]); + + $method = new \ReflectionMethod($driver, '_authenticate'); + $method->setAccessible(true); + + $this->expectException(Horde_Auth_Exception::class); + $method->invoke($driver, 'testuser', ['password' => 'wrongpass']); + } + + /** + * Tests that _authenticate() throws exception when user not found. + * The archaic Horde_Auth design treats _authenticate() as the canonical driver behavior, + * with authenticate() wrapping exceptions to booleans. Testing _authenticate() verifies error codes. + */ + public function testProtectedAuthenticateThrowsExceptionWhenUserNotFound() + { + $this->db->expects($this->once()) + ->method('selectOne') + ->willReturn(null); + + $driver = new Horde_Auth_Sql(['db' => $this->db]); + + $method = new \ReflectionMethod($driver, '_authenticate'); + $method->setAccessible(true); + + $this->expectException(Horde_Auth_Exception::class); + $method->invoke($driver, 'nonexistent', ['password' => 'anypass']); + } +}