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']);
+ }
+}