diff --git a/.horde.yml b/.horde.yml index 13106a22..5da60849 100644 --- a/.horde.yml +++ b/.horde.yml @@ -93,6 +93,7 @@ dependencies: horde/translation: ^3 horde/url: ^3 horde/util: ^3 + horde/oauth: ^4 horde/view: ^3 php81_bc/strftime: ^0.7 ext: @@ -108,9 +109,14 @@ dependencies: sockets: '*' dev: composer: + horde/activesync: ^3 + horde/kolab_server: ^3 + horde/kolab_session: ^3 + horde/ldap: ^3 + horde/mongo: ^2 + horde/routes: ^3 horde/test: ^3 horde/tree: ^3 - horde/routes: ^3 horde/vfs: ^3 autoload: classmap: diff --git a/doc/COMPILING_DI.md b/doc/COMPILING_DI.md new file mode 100644 index 00000000..bc39451a --- /dev/null +++ b/doc/COMPILING_DI.md @@ -0,0 +1,176 @@ +# Compiling a Dependency Injection Binding Map + +Horde's Injector resolves dependencies at runtime through autowiring and +factory/implementation bindings registered during bootstrap. This works +correctly but means every request repeats the same reflection and binder +lookups. A **compiled binding map** captures the result of those lookups as a +plain PHP array that opcache can keep in shared memory, eliminating the +repeated work on subsequent requests. + +Building a complete map is a progressive process: different pages exercise +different code paths and register different bindings. You run profiling across +representative requests, merge the results and then ship the final map as a +static config file. + +## Prerequisites + +The `horde/event-dispatcher` package must be installed: + +``` +composer require horde/event-dispatcher +``` + +It is listed as a suggestion in `horde/injector` and is not required at +runtime. The profiling code path is guarded — if the package is missing, a +warning is logged and the application continues normally. + +## Step 1: Enable profiling + +Set the `HORDE_INJECTOR_PROFILE` environment variable before a request: + +```bash +# Apache — add to virtualhost or .htaccess +SetEnv HORDE_INJECTOR_PROFILE 1 + +# nginx — add to location block +fastcgi_param HORDE_INJECTOR_PROFILE 1; + +# CLI +HORDE_INJECTOR_PROFILE=1 php horde/index.php +``` + +By default the binding map is written to +`sys_get_temp_dir()/horde_injector_bindings.php`. Override the path with a +second variable: + +```bash +HORDE_INJECTOR_PROFILE=1 \ +HORDE_INJECTOR_PROFILE_PATH=/var/cache/horde/bindings.php \ +php horde/index.php +``` + +## Step 2: Exercise the application + +Each request dumps the bindings that were registered during that request. +To build a complete map you need to hit every code path that registers +bindings: + +1. **Log in** to Horde (creates session, auth prefs bindings). +2. **Visit each application** — at minimum load the main page of every + installed app (IMP, Kronolith, Turba, etc.). +3. **Trigger background tasks** — run any CLI cron jobs or AJAX endpoints + that register additional factories. +4. **Exercise administrative pages** — configuration, user management and + permission screens often bind factories that normal pages do not. + +Each request overwrites the output file. To accumulate bindings across +requests, copy or merge files between runs (see Step 3). + +## Step 3: Merge multiple runs + +Each dump is a valid PHP file that returns an associative array: + +```php + ['Horde_Core_Factory_Cache', 'create'], + 'Horde_Db_Adapter' => ['Horde_Core_Factory_Db', 'create'], + 'Horde_Group' => 'Horde_Group_Sql', + // ... +]; +``` + +To combine multiple dumps into a single map: + +```php +write('/var/cache/horde/bindings_merged.php', $merged); +``` + +Later runs add new entries without losing earlier ones. + +## Step 4: Load the compiled map + +Pass the binding map to the Injector constructor: + +```php +$bindings = require '/var/cache/horde/bindings_merged.php'; +$injector = new Horde\Injector\Injector(new Horde\Injector\TopLevel(), $bindings); +``` + +Or load it after construction: + +```php +$injector->loadBindings(require '/var/cache/horde/bindings_merged.php'); +``` + +Bindings loaded this way are additive — they do not remove existing bindings, +and runtime `bindFactory()`/`bindImplementation()` calls still override them. + +## Step 5: Disable profiling + +Remove the environment variables once you have a satisfactory map. With +profiling disabled the Injector's event dispatcher slot remains `null` and +there is zero runtime overhead — the dispatch call sites are guarded by a +null check that the branch predictor eliminates. + +## What gets captured (and what does not) + +The binding map captures two types of bindings: + +- **Implementation bindings** — `'InterfaceName' => 'ConcreteClass'` +- **Factory bindings** — `'InterfaceName' => ['FactoryClass', 'method']` + +Closure bindings cannot be serialized to a PHP array file. They are reported +separately in the Horde log at DEBUG level: + +``` +Injector profiling: 3 uncacheable bindings: Horde\Util\Variables, ... +``` + +Review these after profiling. If a Closure binding is performance-critical, +consider converting it to a factory class so it can be included in the +compiled map. + +## Binding map format reference + +The array format is intentionally simple for opcache efficiency: + +| Value type | Meaning | Example | +|---|---|---| +| `string` | Implementation binding | `'Horde_Cache' => 'Horde_Cache_Storage_File'` | +| `[string, string]` | Factory binding | `'Horde_Db_Adapter' => ['Horde_Core_Factory_Db', 'create']` | + +This is the same format accepted by `Injector::loadBindings()` and produced +by `BindingMapWriter::write()`. + +## Troubleshooting + +**"Injector profiling failed to initialize"** (logged at WARN level) +The EventDispatcher could not be resolved. Verify that `horde/event-dispatcher` +is installed and its factory is registered in the Injector. + +**Output file is not created** +Check that the output directory is writable by the web server process. If +using the default path, verify `sys_get_temp_dir()` returns a writable +directory. + +**Missing bindings after loading the map** +The map only contains bindings that were registered during profiled requests. +Exercise additional code paths and merge the results. Some bindings are +registered conditionally (e.g., based on configuration or installed apps) and +will only appear when those conditions are met. + +**Application behaves differently with the compiled map** +The compiled map provides the same bindings that would have been registered +at runtime. If behavior differs, a Closure binding that performs +request-specific logic may have been replaced by a static Implementation +binding. Check the uncacheable list in the profiling log. diff --git a/js/prototype.js b/js/prototype.js index 21928f57..680df679 100644 --- a/js/prototype.js +++ b/js/prototype.js @@ -7152,6 +7152,15 @@ Form.EventObserver = Class.create(Abstract.EventObserver, { event.memo = memo; element.dispatchEvent(event); + + // Bridge: also fire a native CustomEvent so that vanilla + // addEventListener(eventName, ...) listeners can receive it. + element.dispatchEvent(new CustomEvent(eventName, { + bubbles: bubble, + cancelable: true, + detail: memo + })); + return event; } diff --git a/lib/Horde/Registry.php b/lib/Horde/Registry.php index 1c42d5b7..f6090533 100644 --- a/lib/Horde/Registry.php +++ b/lib/Horde/Registry.php @@ -19,11 +19,13 @@ use Horde\Core\Factory\DriverRepositoryFactory; use Horde\Core\Factory\EventDispatcherFactory; use Horde\Core\Factory\HttpClientFactory; +use Horde\Core\Factory\LoggerFactory; use Horde\Core\Factory\SimpleCacheFactory; use Horde\Core\Factory\TinymceFactory; use Horde\Core\Factory\TinymcePageBinderFactory; use Horde\Core\Horde; use Horde\Editor\Tinymce; +use Horde\Log\Logger as HordeLogger; use Psr\EventDispatcher\EventDispatcherInterface; use Psr\EventDispatcher\ListenerProviderInterface; use Psr\Http\Client\ClientInterface as PsrHttpClientInterface; @@ -467,6 +469,7 @@ public function __construct($session_flags = 0, array $args = []) 'Horde_Template' => 'Horde_Core_Factory_Template', 'Horde_Timezone' => 'Horde_Core_Factory_Timezone', 'Horde_Token' => 'Horde_Core_Factory_Token', + Horde\Token\Token::class => Horde\Core\Factory\TokenServiceFactory::class, 'Horde_Variables' => 'Horde_Core_Factory_Variables', 'Horde_View' => 'Horde_Core_Factory_View', 'Horde_View_Base' => 'Horde_Core_Factory_View', @@ -474,7 +477,8 @@ public function __construct($session_flags = 0, array $args = []) 'Net_DNS2_Resolver' => 'Horde_Core_Factory_Dns', 'Text_LanguageDetect' => 'Horde_Core_Factory_LanguageDetect', Horde\Core\Middleware\AuthHttpBasic::class => Horde\Core\Factory\AuthHttpBasicFactory::class, - Horde\Log\Logger::class => Horde\Core\Factory\LoggerFactory::class, + HordeLogger::class => LoggerFactory::class, + PsrLoggerInterface::class => LoggerFactory::class, 'Horde\\Horde\\Service\\JwtService' => 'Horde\\Horde\\Factory\\JwtServiceFactory', 'Horde\\Horde\\Service\\AuthenticationService' => 'Horde\\Horde\\Factory\\AuthenticationServiceFactory', 'Horde\\Core\\Config\\ConfigLoader' => 'Horde\\Core\\Factory\\ConfigLoaderFactory', @@ -500,7 +504,6 @@ public function __construct($session_flags = 0, array $args = []) /* Define implementations. */ $implementations = [ 'Horde_Controller_ResponseWriter' => 'Horde_Controller_ResponseWriter_Web', - PsrLoggerInterface::class => Horde\Log\Logger::class, RequestFactoryInterface::class => Horde\Http\RequestFactory::class, ]; @@ -551,6 +554,32 @@ function () { set_time_limit($conf['max_exec_time']); } + /* Optional injector profiling — activated by env var. */ + if (getenv('HORDE_INJECTOR_PROFILE')) { + try { + $dispatcher = $injector->getInstance( + EventDispatcherInterface::class + ); + $provider = $injector->getInstance( + ListenerProviderInterface::class + ); + $collector = new \Horde\Injector\BindingMapCollector(); + $provider->addListener($collector); + $injector->setEventDispatcher($dispatcher); + + $dumpPath = getenv('HORDE_INJECTOR_PROFILE_PATH') + ?: sys_get_temp_dir() . '/horde_injector_bindings.php'; + Horde_Shutdown::add( + new \Horde\Core\InjectorProfilingShutdownTask($injector, $dumpPath) + ); + } catch (Throwable $e) { + Horde::log( + 'Injector profiling failed to initialize: ' . $e->getMessage(), + Horde_Log::WARN + ); + } + } + /* The basic framework is up and loaded, so set the init flag. */ $this->hordeInit = true; diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 3759832e..baf4fa02 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,14 +1,26 @@ - - - - test - - - - - lib - src - - + + + + test/Unit + + + test/Integration + + + + + lib + src + + diff --git a/src/Factory/EventDispatcherFactory.php b/src/Factory/EventDispatcherFactory.php index 24fb8222..ace5831b 100644 --- a/src/Factory/EventDispatcherFactory.php +++ b/src/Factory/EventDispatcherFactory.php @@ -21,6 +21,7 @@ use Psr\EventDispatcher\ListenerProviderInterface; use Psr\Log\LoggerInterface; use Psr\Log\NullLogger; +use Throwable; /** * Factory for PSR-14 EventDispatcher and ListenerProvider @@ -58,7 +59,7 @@ public function create(Injector $injector): EventDispatcher try { $logger = $injector->getInstance(LoggerInterface::class); - } catch (\Throwable) { + } catch (Throwable) { $logger = new NullLogger(); } diff --git a/src/Factory/OAuthTokenServiceFactory.php b/src/Factory/OAuthTokenServiceFactory.php new file mode 100644 index 00000000..e26bd890 --- /dev/null +++ b/src/Factory/OAuthTokenServiceFactory.php @@ -0,0 +1,49 @@ + + * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 + */ + +namespace Horde\Core\Factory; + +use Horde\Core\Config\ConfigLoader; +use Horde\Core\Service\NullOAuthTokenService; +use Horde\Core\Service\OAuthTokenService; +use Horde_Injector; + +/** + * Factory for OAuthTokenService. + * + * Returns the configured token service implementation. Defaults to + * NullOAuthTokenService when OAuth is not configured. + * + * @category Horde + * @package Core + * @author Ralf Lang + * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 + */ +class OAuthTokenServiceFactory +{ + public function create(Horde_Injector $injector): OAuthTokenService + { + $loader = $injector->getInstance(ConfigLoader::class); + $state = $loader->load('horde'); + + $driver = $state->get('oauth.token_driver', 'null'); + + return match (strtolower($driver)) { + 'null', '' => new NullOAuthTokenService(), + default => new NullOAuthTokenService(), + }; + } +} diff --git a/src/Factory/SimpleCacheFactory.php b/src/Factory/SimpleCacheFactory.php index 5373baa4..c9ba472b 100644 --- a/src/Factory/SimpleCacheFactory.php +++ b/src/Factory/SimpleCacheFactory.php @@ -28,6 +28,7 @@ use Horde_HashTable_Base; use Psr\Log\LoggerInterface; use Psr\Log\NullLogger; +use Throwable; /** * Factory for PSR-16 SimpleCache (Horde\Cache\Cache) @@ -85,7 +86,7 @@ private function getLogger(Injector $injector): LoggerInterface { try { return $injector->getInstance(LoggerInterface::class); - } catch (\Throwable) { + } catch (Throwable) { return new NullLogger(); } } diff --git a/src/Factory/TokenServiceFactory.php b/src/Factory/TokenServiceFactory.php new file mode 100644 index 00000000..d6dab7d2 --- /dev/null +++ b/src/Factory/TokenServiceFactory.php @@ -0,0 +1,105 @@ + + * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 + */ + +namespace Horde\Core\Factory; + +use Horde; +use Horde\Token\Token; +use Horde\Token\TokenConfig; +use Horde_Injector; +use Horde_String; +use Horde_Support_Randomid; + +/** + * Factory for the modern Horde\Token\Token CSRF service. + * + * Reads the same $conf['token'] settings as the legacy + * Horde_Core_Factory_Token and produces a PSR-4 Token facade + * backed by the configured storage driver (SQL, file, or null). + * + * The HMAC secret is sourced from the session, matching the + * legacy factory behaviour so both old and new token services + * generate compatible signatures within the same session. + * + * @category Horde + * @package Core + * @author Ralf Lang + * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 + */ +class TokenServiceFactory +{ + public function create(Horde_Injector $injector): Token + { + global $conf, $session; + + // Determine driver from config (same source as legacy factory) + $driver = empty($conf['token']) + ? 'null' + : Horde_String::lower($conf['token']['driver']); + + if ($driver === 'none') { + $driver = 'null'; + } + + // Source the HMAC secret from the session (same as legacy factory) + if (!$session->exists('horde', 'token_secret_key')) { + $session->set( + 'horde', + 'token_secret_key', + strval(new Horde_Support_Randomid()) + ); + } + $secret = $session->get('horde', 'token_secret_key'); + + // Driver-specific params from conf.php + $params = empty($conf['token']) + ? [] + : Horde::getDriverConfig('token', $conf['token']['driver']); + + // Token lifetime: conf value is in minutes, convert to seconds. + // Default -1 = no expiry (matches TokenConfig default). + $lifetimeSeconds = isset($conf['urls']['token_lifetime']) + ? (int) $conf['urls']['token_lifetime'] * 60 + : -1; + + // Storage cleanup timeout (seconds), default 24 hours. + $timeout = isset($params['timeout']) + ? (int) $params['timeout'] + : 86400; + + $config = new TokenConfig( + secret: $secret, + tokenLifetime: $lifetimeSeconds, + timeout: $timeout + ); + + return match ($driver) { + 'sql' => Token::sql( + $secret, + $injector->getInstance('Horde_Core_Factory_Db') + ->create('horde', 'token'), + $config, + $params['table'] ?? 'horde_tokens' + ), + 'file' => Token::file( + $secret, + $params['token_dir'] ?? Horde::getTempDir(), + $config + ), + default => Token::null($secret, $config), + }; + } +} diff --git a/src/InjectorProfilingShutdownTask.php b/src/InjectorProfilingShutdownTask.php new file mode 100644 index 00000000..c951f2ca --- /dev/null +++ b/src/InjectorProfilingShutdownTask.php @@ -0,0 +1,61 @@ +extractBindings($this->injector); + $writer->write($this->outputPath, $result['cacheable']); + + if ($result['uncacheable'] !== []) { + Horde::log( + 'Injector profiling: ' . count($result['uncacheable']) + . ' uncacheable bindings: ' . implode(', ', $result['uncacheable']), + Horde_Log::DEBUG + ); + } + + Horde::log( + 'Injector profiling: wrote ' . count($result['cacheable']) + . ' bindings to ' . $this->outputPath, + Horde_Log::INFO + ); + } +} diff --git a/src/Service/Exception/OAuthTokenNotFoundException.php b/src/Service/Exception/OAuthTokenNotFoundException.php new file mode 100644 index 00000000..768fced4 --- /dev/null +++ b/src/Service/Exception/OAuthTokenNotFoundException.php @@ -0,0 +1,29 @@ + + * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 + */ + +namespace Horde\Core\Service\Exception; + +use RuntimeException; + +/** + * Exception thrown when no OAuth tokens exist for a user/provider pair + * + * @category Horde + * @package Core + * @author Ralf Lang + * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 + */ +class OAuthTokenNotFoundException extends RuntimeException {} diff --git a/src/Service/NullOAuthTokenService.php b/src/Service/NullOAuthTokenService.php new file mode 100644 index 00000000..7b38b668 --- /dev/null +++ b/src/Service/NullOAuthTokenService.php @@ -0,0 +1,56 @@ + + * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 + */ + +namespace Horde\Core\Service; + +use Horde\Core\Service\Exception\OAuthTokenNotFoundException; +use Horde\Oauth\Client\TokenSet; + +/** + * Null OAuth token service — safe default when OAuth is not configured. + * + * Store is silently discarded. Any retrieval throws OAuthTokenNotFoundException. + * + * @category Horde + * @package Core + * @author Ralf Lang + * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 + */ +class NullOAuthTokenService implements OAuthTokenService +{ + public function getAccessToken(string $userId, string $providerId): string + { + throw new OAuthTokenNotFoundException( + "No OAuth tokens for user '{$userId}' / provider '{$providerId}'" + ); + } + + public function store(string $userId, string $providerId, TokenSet $tokens): void {} + + public function hasTokens(string $userId, string $providerId): bool + { + return false; + } + + public function remove(string $userId, string $providerId): void {} + + public function getTokenSet(string $userId, string $providerId): TokenSet + { + throw new OAuthTokenNotFoundException( + "No OAuth tokens for user '{$userId}' / provider '{$providerId}'" + ); + } +} diff --git a/src/Service/OAuthTokenService.php b/src/Service/OAuthTokenService.php new file mode 100644 index 00000000..057698b8 --- /dev/null +++ b/src/Service/OAuthTokenService.php @@ -0,0 +1,82 @@ + + * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 + */ + +namespace Horde\Core\Service; + +use Horde\Core\Factory\OAuthTokenServiceFactory; +use Horde\Injector\Attribute\Factory; +use Horde\Oauth\Client\TokenSet; + +/** + * Centralized OAuth2 token storage and retrieval. + * + * Stores access/refresh tokens per user and provider. Handles transparent + * token refresh when access tokens expire. Consumed by IMAP/SMTP for + * XOAUTH2 authentication and by any component needing OAuth2 tokens. + * + * @category Horde + * @package Core + * @author Ralf Lang + * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 + */ +#[Factory(factory: OAuthTokenServiceFactory::class, method: 'create')] +interface OAuthTokenService +{ + /** + * Get a valid access token, refreshing transparently if expired. + * + * @param string $userId Horde user ID + * @param string $providerId Provider identifier (e.g. 'google', 'microsoft') + * @return string The access token string + * @throws Exception\OAuthTokenNotFoundException + */ + public function getAccessToken(string $userId, string $providerId): string; + + /** + * Store a token set after initial authorization or refresh. + * + * @param string $userId Horde user ID + * @param string $providerId Provider identifier + * @param TokenSet $tokens Token set from OAuth2 flow + */ + public function store(string $userId, string $providerId, TokenSet $tokens): void; + + /** + * Check if a user has stored tokens for a provider. + * + * @param string $userId Horde user ID + * @param string $providerId Provider identifier + */ + public function hasTokens(string $userId, string $providerId): bool; + + /** + * Remove stored tokens (disconnect/revoke). + * + * @param string $userId Horde user ID + * @param string $providerId Provider identifier + */ + public function remove(string $userId, string $providerId): void; + + /** + * Get the full token set when the caller needs more than the access token. + * + * @param string $userId Horde user ID + * @param string $providerId Provider identifier + * @return TokenSet The stored token set + * @throws Exception\OAuthTokenNotFoundException + */ + public function getTokenSet(string $userId, string $providerId): TokenSet; +} diff --git a/test/AllTests.php b/test/AllTests.php deleted file mode 100644 index ad725e6f..00000000 --- a/test/AllTests.php +++ /dev/null @@ -1,6 +0,0 @@ -run(); diff --git a/test/Config/ConfigMetadataIntegrationTest.php b/test/Integration/Config/ConfigMetadataIntegrationTest.php similarity index 97% rename from test/Config/ConfigMetadataIntegrationTest.php rename to test/Integration/Config/ConfigMetadataIntegrationTest.php index a5b680fa..0c9869cc 100644 --- a/test/Config/ConfigMetadataIntegrationTest.php +++ b/test/Integration/Config/ConfigMetadataIntegrationTest.php @@ -15,21 +15,22 @@ * @package Core */ -namespace Horde\Core\Test\Config; +namespace Horde\Core\Test\Integration\Config; use Horde\Core\Config\ConfigMetadataProvider; use Horde\Core\Config\ConfigStateWithMetadata; use Horde\Core\Config\Driver\DriverRepository; use Horde\Core\Config\Driver\Sql\MySQLDriver; use Horde\Core\Config\Legacy\LegacyConfigAdapter; +use PHPUnit\Framework\Attributes\CoversNothing; use PHPUnit\Framework\TestCase; /** * Integration tests for the config metadata system. * * Tests the complete flow from drivers through provider to state. - * @coversNothing */ +#[CoversNothing] class ConfigMetadataIntegrationTest extends TestCase { private DriverRepository $repository; diff --git a/test/Config/Driver/AllDriversInstantiationTest.php b/test/Integration/Config/Driver/AllDriversInstantiationTest.php similarity index 96% rename from test/Config/Driver/AllDriversInstantiationTest.php rename to test/Integration/Config/Driver/AllDriversInstantiationTest.php index e7a5829d..35050920 100644 --- a/test/Config/Driver/AllDriversInstantiationTest.php +++ b/test/Integration/Config/Driver/AllDriversInstantiationTest.php @@ -14,10 +14,11 @@ * @package Core */ -namespace Horde\Core\Test\Config\Driver; +namespace Horde\Core\Test\Integration\Config\Driver; use Horde\Core\Config\Driver\DriverInterface; use Horde\Core\Config\Metadata\PropertyMetadata; +use PHPUnit\Framework\Attributes\CoversNothing; use PHPUnit\Framework\TestCase; use RecursiveDirectoryIterator; use RecursiveIteratorIterator; @@ -37,8 +38,8 @@ * @copyright 2026 The Horde Project * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 * @package Core - * @coversNothing */ +#[CoversNothing] class AllDriversInstantiationTest extends TestCase { /** @@ -110,7 +111,7 @@ public function testAllDriversCanBeInstantiated(): void private function discoverAllDriverClasses(): array { $classes = []; - $path = __DIR__ . '/../../../src/Config/Driver'; + $path = __DIR__ . '/../../../../src/Config/Driver'; $iterator = new RecursiveIteratorIterator( new RecursiveDirectoryIterator($path) diff --git a/test/Config/Legacy/HordeConfigIntegrationTest.php b/test/Integration/Config/Legacy/HordeConfigIntegrationTest.php similarity index 98% rename from test/Config/Legacy/HordeConfigIntegrationTest.php rename to test/Integration/Config/Legacy/HordeConfigIntegrationTest.php index 3cb6e5dd..d99e17fb 100644 --- a/test/Config/Legacy/HordeConfigIntegrationTest.php +++ b/test/Integration/Config/Legacy/HordeConfigIntegrationTest.php @@ -14,7 +14,7 @@ * @package Core */ -namespace Horde\Core\Test\Config\Legacy; +namespace Horde\Core\Test\Integration\Config\Legacy; use Horde\Core\Config\ConfigMetadataProvider; use Horde\Core\Config\Driver\DriverRepository; @@ -23,6 +23,7 @@ use Horde\Core\Config\Driver\Auth\SqlAuthDriver; use Horde\Core\Config\Driver\Auth\LdapAuthDriver; use Horde\Core\Config\Legacy\LegacyConfigAdapter; +use PHPUnit\Framework\Attributes\CoversNothing; use PHPUnit\Framework\TestCase; /** @@ -38,8 +39,8 @@ * @copyright 2026 The Horde Project * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 * @package Core - * @coversNothing */ +#[CoversNothing] class HordeConfigIntegrationTest extends TestCase { /** diff --git a/test/Config/LegacyConfigSQLIntegrationTest.php b/test/Integration/Config/LegacyConfigSQLIntegrationTest.php similarity index 99% rename from test/Config/LegacyConfigSQLIntegrationTest.php rename to test/Integration/Config/LegacyConfigSQLIntegrationTest.php index 579726d3..a51e22dc 100644 --- a/test/Config/LegacyConfigSQLIntegrationTest.php +++ b/test/Integration/Config/LegacyConfigSQLIntegrationTest.php @@ -15,7 +15,7 @@ * @package Core */ -namespace Horde\Core\Test\Config; +namespace Horde\Core\Test\Integration\Config; use Horde\Core\Config\ConfigMetadataProvider; use Horde\Core\Config\Driver\DriverRepository; diff --git a/test/RampageIntegrationTest.php b/test/Integration/RampageIntegrationTest.php similarity index 98% rename from test/RampageIntegrationTest.php rename to test/Integration/RampageIntegrationTest.php index 8c4d3100..c1aa0de3 100644 --- a/test/RampageIntegrationTest.php +++ b/test/Integration/RampageIntegrationTest.php @@ -1,5 +1,7 @@ * @category Horde * @package Core - * @coversNothing */ +#[CoversNothing] #[Group('integration')] class RampageIntegrationTest extends TestCase { diff --git a/test/Horde_Themes_Css_Compress_IntegrationTest.php b/test/Integration/Themes/Css/CompressIntegrationTest.php similarity index 61% rename from test/Horde_Themes_Css_Compress_IntegrationTest.php rename to test/Integration/Themes/Css/CompressIntegrationTest.php index f5b4f365..f371db13 100644 --- a/test/Horde_Themes_Css_Compress_IntegrationTest.php +++ b/test/Integration/Themes/Css/CompressIntegrationTest.php @@ -1,5 +1,7 @@ assertTrue(class_exists('Horde\CssMinify\CssParserMinifier')); - $this->assertTrue(class_exists('Horde\CssMinify\Input\CssFile')); - $this->assertTrue(class_exists('Horde\CssMinify\Input\FileCollectionInput')); - $this->assertTrue(class_exists('Horde\CssMinify\Settings')); - $this->assertTrue(class_exists('Horde\CssMinify\UrlCallback')); - $this->assertTrue(class_exists('Horde\CssMinify\ImportCallback')); + $this->assertTrue(class_exists(CssParserMinifier::class)); + $this->assertTrue(class_exists(CssFile::class)); + $this->assertTrue(class_exists(FileCollectionInput::class)); + $this->assertTrue(class_exists(Settings::class)); + $this->assertTrue(class_exists(UrlCallback::class)); + $this->assertTrue(class_exists(ImportCallback::class)); } public function testCssFileValidation(): void @@ -29,7 +46,7 @@ public function testCssFileValidation(): void file_put_contents($tempFile, 'body { color: red; }'); // Modern API validates at construction - $file = new Horde\CssMinify\Input\CssFile('test.css', $tempFile); + $file = new CssFile('test.css', $tempFile); $this->assertSame('test.css', $file->uri); $this->assertSame($tempFile, $file->filepath); @@ -42,7 +59,7 @@ public function testCssFileThrowsOnInvalid(): void $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('File not readable'); - new Horde\CssMinify\Input\CssFile('missing.css', '/nonexistent/path.css'); + new CssFile('missing.css', '/nonexistent/path.css'); } public function testModernApiTypeSafety(): void @@ -50,9 +67,9 @@ public function testModernApiTypeSafety(): void $tempFile = tempnam(sys_get_temp_dir(), 'css'); file_put_contents($tempFile, 'body { color: red; }'); - $file = new Horde\CssMinify\Input\CssFile('test.css', $tempFile); - $collection = new Horde\CssMinify\Input\FileCollectionInput($file); - $minifier = new Horde\CssMinify\CssParserMinifier($collection); + $file = new CssFile('test.css', $tempFile); + $collection = new FileCollectionInput($file); + $minifier = new CssParserMinifier($collection); $result = $minifier->minify(); @@ -64,8 +81,8 @@ public function testModernApiTypeSafety(): void public function testSettingsImmutability(): void { - $urlCallback = new Horde\CssMinify\UrlCallback(fn($p) => $p); - $settings = new Horde\CssMinify\Settings(dataUrlCallback: $urlCallback); + $urlCallback = new UrlCallback(fn($p) => $p); + $settings = new Settings(dataUrlCallback: $urlCallback); // Properties are readonly $this->assertSame($urlCallback, $settings->dataUrlCallback); @@ -74,7 +91,7 @@ public function testSettingsImmutability(): void public function testCompressCodePath(): void { // This verifies the updated Compress.php code paths compile - $reflection = new ReflectionClass('Horde_Themes_Css_Compress'); + $reflection = new ReflectionClass(Horde_Themes_Css_Compress::class); // Verify compress method exists $this->assertTrue($reflection->hasMethod('compress')); diff --git a/test/Mock/MockConnector.php b/test/Mock/MockConnector.php index 1fec0b91..77ed11b9 100644 --- a/test/Mock/MockConnector.php +++ b/test/Mock/MockConnector.php @@ -1,6 +1,6 @@ expectException(NotFoundException::class); $credential->get('password'); } - } diff --git a/test/Config/BackendConfigLoaderTest.php b/test/Unit/Config/BackendConfigLoaderTest.php similarity index 98% rename from test/Config/BackendConfigLoaderTest.php rename to test/Unit/Config/BackendConfigLoaderTest.php index 1a72429a..90544f55 100644 --- a/test/Config/BackendConfigLoaderTest.php +++ b/test/Unit/Config/BackendConfigLoaderTest.php @@ -2,8 +2,11 @@ declare(strict_types=1); -namespace Horde\Core\Config; +namespace Horde\Core\Test\Unit\Config; +use Horde\Core\Config\BackendConfigLoader; +use Horde\Core\Config\BackendState; +use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; /** @@ -14,7 +17,7 @@ * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 * @package Core */ -#[\PHPUnit\Framework\Attributes\CoversClass(BackendConfigLoader::class)] +#[CoversClass(BackendConfigLoader::class)] class BackendConfigLoaderTest extends TestCase { private string $tempDir; diff --git a/test/Config/BackendStateTest.php b/test/Unit/Config/BackendStateTest.php similarity index 95% rename from test/Config/BackendStateTest.php rename to test/Unit/Config/BackendStateTest.php index 118f9f22..fe7413d7 100644 --- a/test/Config/BackendStateTest.php +++ b/test/Unit/Config/BackendStateTest.php @@ -2,8 +2,10 @@ declare(strict_types=1); -namespace Horde\Core\Config; +namespace Horde\Core\Test\Unit\Config; +use Horde\Core\Config\BackendState; +use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; /** @@ -14,7 +16,7 @@ * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 * @package Core */ -#[\PHPUnit\Framework\Attributes\CoversClass(BackendState::class)] +#[CoversClass(BackendState::class)] class BackendStateTest extends TestCase { private BackendState $state; diff --git a/test/Config/ConfigMetadataProviderTest.php b/test/Unit/Config/ConfigMetadataProviderTest.php similarity index 99% rename from test/Config/ConfigMetadataProviderTest.php rename to test/Unit/Config/ConfigMetadataProviderTest.php index f90843cc..df88333c 100644 --- a/test/Config/ConfigMetadataProviderTest.php +++ b/test/Unit/Config/ConfigMetadataProviderTest.php @@ -15,7 +15,7 @@ * @package Core */ -namespace Horde\Core\Test\Config; +namespace Horde\Core\Test\Unit\Config; use Horde\Core\Config\ConfigMetadataProvider; use Horde\Core\Config\Driver\DriverRepository; diff --git a/test/Config/ConfigStateWithMetadataTest.php b/test/Unit/Config/ConfigStateWithMetadataTest.php similarity index 99% rename from test/Config/ConfigStateWithMetadataTest.php rename to test/Unit/Config/ConfigStateWithMetadataTest.php index 5dcf137b..b64ba7ea 100644 --- a/test/Config/ConfigStateWithMetadataTest.php +++ b/test/Unit/Config/ConfigStateWithMetadataTest.php @@ -14,7 +14,7 @@ * @package Core */ -namespace Horde\Core\Test\Config; +namespace Horde\Core\Test\Unit\Config; use Horde\Core\Config\ConfigMetadataProvider; use Horde\Core\Config\ConfigStateWithMetadata; diff --git a/test/Config/Driver/DriverRepositoryTest.php b/test/Unit/Config/Driver/DriverRepositoryTest.php similarity index 98% rename from test/Config/Driver/DriverRepositoryTest.php rename to test/Unit/Config/Driver/DriverRepositoryTest.php index 1ab29500..d0a93e0d 100644 --- a/test/Config/Driver/DriverRepositoryTest.php +++ b/test/Unit/Config/Driver/DriverRepositoryTest.php @@ -15,7 +15,7 @@ * @package Core */ -namespace Horde\Core\Test\Config\Driver; +namespace Horde\Core\Test\Unit\Config\Driver; use Horde\Core\Config\Driver\DriverRepository; use Horde\Core\Config\Driver\Sql\MySQLDriver; diff --git a/test/Config/Driver/EnumOptionsFormatTest.php b/test/Unit/Config/Driver/EnumOptionsFormatTest.php similarity index 97% rename from test/Config/Driver/EnumOptionsFormatTest.php rename to test/Unit/Config/Driver/EnumOptionsFormatTest.php index b82ecd61..b7ef3849 100644 --- a/test/Config/Driver/EnumOptionsFormatTest.php +++ b/test/Unit/Config/Driver/EnumOptionsFormatTest.php @@ -14,11 +14,12 @@ * @package Core */ -namespace Horde\Core\Test\Config\Driver; +namespace Horde\Core\Test\Unit\Config\Driver; use Horde\Core\Config\Driver\DriverInterface; use Horde\Core\Config\Metadata\FieldType; use Horde\Core\Config\Metadata\PropertyMetadata; +use PHPUnit\Framework\Attributes\CoversNothing; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use RecursiveDirectoryIterator; @@ -45,8 +46,8 @@ * @copyright 2026 The Horde Project * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 * @package Core - * @coversNothing */ +#[CoversNothing] class EnumOptionsFormatTest extends TestCase { /** @@ -195,7 +196,7 @@ public static function allDriversProvider(): array private static function discoverAllDriverClasses(): array { $classes = []; - $path = __DIR__ . '/../../../src/Config/Driver'; + $path = __DIR__ . '/../../../../src/Config/Driver'; $iterator = new RecursiveIteratorIterator( new RecursiveDirectoryIterator($path) diff --git a/test/Config/Driver/EnumValidationTest.php b/test/Unit/Config/Driver/EnumValidationTest.php similarity index 97% rename from test/Config/Driver/EnumValidationTest.php rename to test/Unit/Config/Driver/EnumValidationTest.php index 054ea91a..f162518a 100644 --- a/test/Config/Driver/EnumValidationTest.php +++ b/test/Unit/Config/Driver/EnumValidationTest.php @@ -14,12 +14,13 @@ * @package Core */ -namespace Horde\Core\Test\Config\Driver; +namespace Horde\Core\Test\Unit\Config\Driver; use Horde\Core\Config\Metadata\FieldType; use Horde\Core\Config\Metadata\PropertyMetadata; -use PHPUnit\Framework\TestCase; +use PHPUnit\Framework\Attributes\CoversNothing; use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\TestCase; use RecursiveDirectoryIterator; use RecursiveIteratorIterator; use Throwable; @@ -39,8 +40,8 @@ * @copyright 2026 The Horde Project * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 * @package Core - * @coversNothing */ +#[CoversNothing] class EnumValidationTest extends TestCase { /** @@ -228,7 +229,7 @@ private static function extractAllFieldsRecursive(object $driver): array private static function discoverAllDriverClasses(): array { $classes = []; - $path = __DIR__ . '/../../../src/Config/Driver'; + $path = __DIR__ . '/../../../../src/Config/Driver'; $iterator = new RecursiveIteratorIterator( new RecursiveDirectoryIterator($path) diff --git a/test/Config/Driver/PropertyMetadataParameterTest.php b/test/Unit/Config/Driver/PropertyMetadataParameterTest.php similarity index 97% rename from test/Config/Driver/PropertyMetadataParameterTest.php rename to test/Unit/Config/Driver/PropertyMetadataParameterTest.php index f8b42859..bb9b6b51 100644 --- a/test/Config/Driver/PropertyMetadataParameterTest.php +++ b/test/Unit/Config/Driver/PropertyMetadataParameterTest.php @@ -14,8 +14,9 @@ * @package Core */ -namespace Horde\Core\Test\Config\Driver; +namespace Horde\Core\Test\Unit\Config\Driver; +use PHPUnit\Framework\Attributes\CoversNothing; use PHPUnit\Framework\TestCase; use RecursiveDirectoryIterator; use RecursiveIteratorIterator; @@ -34,8 +35,8 @@ * @copyright 2026 The Horde Project * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 * @package Core - * @coversNothing */ +#[CoversNothing] class PropertyMetadataParameterTest extends TestCase { /** @@ -163,7 +164,7 @@ public function testDiagnosticParameterUsage(): void private function discoverAllDriverClasses(): array { $classes = []; - $path = __DIR__ . '/../../../src/Config/Driver'; + $path = __DIR__ . '/../../../../src/Config/Driver'; $iterator = new RecursiveIteratorIterator( new RecursiveDirectoryIterator($path) diff --git a/test/Config/Legacy/CompleteLegacyFormatTest.php b/test/Unit/Config/Legacy/CompleteLegacyFormatTest.php similarity index 98% rename from test/Config/Legacy/CompleteLegacyFormatTest.php rename to test/Unit/Config/Legacy/CompleteLegacyFormatTest.php index 98372d44..742ac402 100644 --- a/test/Config/Legacy/CompleteLegacyFormatTest.php +++ b/test/Unit/Config/Legacy/CompleteLegacyFormatTest.php @@ -14,13 +14,14 @@ * @package Core */ -namespace Horde\Core\Test\Config\Legacy; +namespace Horde\Core\Test\Unit\Config\Legacy; use Horde\Core\Config\ConfigMetadataProvider; use Horde\Core\Config\Driver\DriverRepository; use Horde\Core\Config\Metadata\FieldType; -use PHPUnit\Framework\TestCase; +use PHPUnit\Framework\Attributes\CoversNothing; use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\TestCase; use RecursiveDirectoryIterator; use RecursiveIteratorIterator; use Throwable; @@ -39,8 +40,8 @@ * @copyright 2026 The Horde Project * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 * @package Core - * @coversNothing */ +#[CoversNothing] class CompleteLegacyFormatTest extends TestCase { private DriverRepository $repository; @@ -239,7 +240,7 @@ public static function allDriversProvider(): array private static function discoverAllDriverClasses(): array { $classes = []; - $path = __DIR__ . '/../../../src/Config/Driver'; + $path = __DIR__ . '/../../../../src/Config/Driver'; $iterator = new RecursiveIteratorIterator( new RecursiveDirectoryIterator($path) diff --git a/test/Config/Legacy/LegacyConfigAdapterTest.php b/test/Unit/Config/Legacy/LegacyConfigAdapterTest.php similarity index 99% rename from test/Config/Legacy/LegacyConfigAdapterTest.php rename to test/Unit/Config/Legacy/LegacyConfigAdapterTest.php index 0ebde12a..fec9a929 100644 --- a/test/Config/Legacy/LegacyConfigAdapterTest.php +++ b/test/Unit/Config/Legacy/LegacyConfigAdapterTest.php @@ -15,7 +15,7 @@ * @package Core */ -namespace Horde\Core\Test\Config\Legacy; +namespace Horde\Core\Test\Unit\Config\Legacy; use Horde\Core\Config\ConfigMetadataProvider; use Horde\Core\Config\Driver\DriverRepository; diff --git a/test/Config/LegacyMergedConfigInjectorTest.php b/test/Unit/Config/LegacyMergedConfigInjectorTest.php similarity index 94% rename from test/Config/LegacyMergedConfigInjectorTest.php rename to test/Unit/Config/LegacyMergedConfigInjectorTest.php index d9503608..b4462aca 100644 --- a/test/Config/LegacyMergedConfigInjectorTest.php +++ b/test/Unit/Config/LegacyMergedConfigInjectorTest.php @@ -14,7 +14,7 @@ * @package Core */ -namespace Horde\Core\Test\Config; +namespace Horde\Core\Test\Unit\Config; use Exception; use Horde\Core\Config\LegacyMergedConfig; @@ -23,6 +23,7 @@ use Horde_Injector_TopLevel; use Horde_Registry_Hordeconfig; use Horde_Registry_Hordeconfig_Merged; +use PHPUnit\Framework\Attributes\CoversNothing; use PHPUnit\Framework\TestCase; use RuntimeException; @@ -36,8 +37,8 @@ * @copyright 2026 The Horde Project * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 * @package Core - * @coversNothing */ +#[CoversNothing] class LegacyMergedConfigInjectorTest extends TestCase { private Horde_Injector $injector; @@ -77,7 +78,7 @@ public function testRebindingUpdatesInjectorInstance(): void $this->assertEquals('horde', $retrieved1->get('app')); $this->assertEquals('file', $retrieved1->get('cache.driver')); - // Rebind (simulates pushApp('imp') → importConfig('imp')) + // Rebind (simulates pushApp('imp') -> importConfig('imp')) $impConfig = new LegacyMergedConfig(['app' => 'imp', 'cache' => ['driver' => 'memcache']]); $this->injector->setInstance(LegacyMergedConfig::class, $impConfig); @@ -93,9 +94,9 @@ public function testRebindingUpdatesInjectorInstance(): void * Test pushApp/popApp simulation with config context switching. * * Simulates: - * 1. appInit('horde') → importConfig('horde') - * 2. pushApp('imp') → importConfig('imp') - * 3. popApp() → importConfig('horde') + * 1. appInit('horde') -> importConfig('horde') + * 2. pushApp('imp') -> importConfig('imp') + * 3. popApp() -> importConfig('horde') */ public function testSimulatePushPopAppConfigSwitching(): void { @@ -110,7 +111,7 @@ public function testSimulatePushPopAppConfigSwitching(): void $this->assertEquals('horde', $config1->get('app')); $this->assertEquals('horde-db', $config1->get('database.host')); - // Simulate pushApp('imp') → importConfig('imp') + // Simulate pushApp('imp') -> importConfig('imp') // (imp config merged with horde config) $impConfig = new LegacyMergedConfig([ 'app' => 'imp', @@ -124,7 +125,7 @@ public function testSimulatePushPopAppConfigSwitching(): void $this->assertEquals('horde-db', $config2->get('database.host')); $this->assertEquals('imap.example.com', $config2->get('mail.server')); - // Simulate popApp() → importConfig('horde') + // Simulate popApp() -> importConfig('horde') // (restore horde config) $this->injector->setInstance(LegacyMergedConfig::class, $hordeConfig); diff --git a/test/Config/LegacyMergedConfigTest.php b/test/Unit/Config/LegacyMergedConfigTest.php similarity index 99% rename from test/Config/LegacyMergedConfigTest.php rename to test/Unit/Config/LegacyMergedConfigTest.php index b343b7cd..4b97952b 100644 --- a/test/Config/LegacyMergedConfigTest.php +++ b/test/Unit/Config/LegacyMergedConfigTest.php @@ -14,7 +14,7 @@ * @package Core */ -namespace Horde\Core\Test\Config; +namespace Horde\Core\Test\Unit\Config; use Horde\Core\Config\LegacyMergedConfig; use Horde\Core\Config\State; diff --git a/test/Config/LoggerConfigTest.php b/test/Unit/Config/LoggerConfigTest.php similarity index 99% rename from test/Config/LoggerConfigTest.php rename to test/Unit/Config/LoggerConfigTest.php index 35e92e2d..14ff120a 100644 --- a/test/Config/LoggerConfigTest.php +++ b/test/Unit/Config/LoggerConfigTest.php @@ -1,5 +1,7 @@ [$field], - 'مرحبا' => [$field], + "\u{65E5}\u{672C}\u{8A9E}" => [$field], + "\u{0645}\u{0631}\u{062D}\u{0628}\u{0627}" => [$field], ]); - $this->assertTrue($conditional->hasCase('日本語')); - $this->assertTrue($conditional->hasCase('مرحبا')); + $this->assertTrue($conditional->hasCase("\u{65E5}\u{672C}\u{8A9E}")); + $this->assertTrue($conditional->hasCase("\u{0645}\u{0631}\u{062D}\u{0628}\u{0627}")); } public function testLargeFieldArraysPerCase(): void diff --git a/test/Config/Metadata/PropertyMetadataTest.php b/test/Unit/Config/Metadata/PropertyMetadataTest.php similarity index 99% rename from test/Config/Metadata/PropertyMetadataTest.php rename to test/Unit/Config/Metadata/PropertyMetadataTest.php index 6cf092da..adb50890 100644 --- a/test/Config/Metadata/PropertyMetadataTest.php +++ b/test/Unit/Config/Metadata/PropertyMetadataTest.php @@ -15,7 +15,7 @@ * @package Core */ -namespace Horde\Core\Test\Config\Metadata; +namespace Horde\Core\Test\Unit\Config\Metadata; use Horde\Core\Config\Metadata\FieldType; use Horde\Core\Config\Metadata\PropertyMetadata; diff --git a/test/Config/Metadata/ValidationResultTest.php b/test/Unit/Config/Metadata/ValidationResultTest.php similarity index 99% rename from test/Config/Metadata/ValidationResultTest.php rename to test/Unit/Config/Metadata/ValidationResultTest.php index a26447ea..f5c03868 100644 --- a/test/Config/Metadata/ValidationResultTest.php +++ b/test/Unit/Config/Metadata/ValidationResultTest.php @@ -14,7 +14,7 @@ * @package Core */ -namespace Horde\Core\Test\Config\Metadata; +namespace Horde\Core\Test\Unit\Config\Metadata; use Horde\Core\Config\Metadata\ValidationResult; use PHPUnit\Framework\Attributes\CoversClass; diff --git a/test/Config/PrefsConfigLoaderTest.php b/test/Unit/Config/PrefsConfigLoaderTest.php similarity index 99% rename from test/Config/PrefsConfigLoaderTest.php rename to test/Unit/Config/PrefsConfigLoaderTest.php index 8468cdc2..007e3c79 100644 --- a/test/Config/PrefsConfigLoaderTest.php +++ b/test/Unit/Config/PrefsConfigLoaderTest.php @@ -14,13 +14,13 @@ * @package Core */ -namespace Horde\Core\Test\Config; +namespace Horde\Core\Test\Unit\Config; +use Closure; use Horde\Core\Config\PrefsConfigLoader; use Horde\Core\Config\PrefsState; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; -use Closure; /** * Tests for PrefsConfigLoader. diff --git a/test/Config/PrefsStateTest.php b/test/Unit/Config/PrefsStateTest.php similarity index 95% rename from test/Config/PrefsStateTest.php rename to test/Unit/Config/PrefsStateTest.php index ba26d0fa..6adb0200 100644 --- a/test/Config/PrefsStateTest.php +++ b/test/Unit/Config/PrefsStateTest.php @@ -2,8 +2,10 @@ declare(strict_types=1); -namespace Horde\Core\Config; +namespace Horde\Core\Test\Unit\Config; +use Horde\Core\Config\PrefsState; +use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; /** @@ -14,7 +16,7 @@ * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 * @package Core */ -#[\PHPUnit\Framework\Attributes\CoversClass(PrefsState::class)] +#[CoversClass(PrefsState::class)] class PrefsStateTest extends TestCase { private PrefsState $state; diff --git a/test/Config/RegistryConfigLoaderTest.php b/test/Unit/Config/RegistryConfigLoaderTest.php similarity index 99% rename from test/Config/RegistryConfigLoaderTest.php rename to test/Unit/Config/RegistryConfigLoaderTest.php index 6f831bd9..1cea15f9 100644 --- a/test/Config/RegistryConfigLoaderTest.php +++ b/test/Unit/Config/RegistryConfigLoaderTest.php @@ -14,7 +14,7 @@ * @package Core */ -namespace Horde\Core\Test\Config; +namespace Horde\Core\Test\Unit\Config; use Horde\Core\Config\RegistryConfigLoader; use Horde\Core\Config\RegistryState; diff --git a/test/Config/RegistryStateTest.php b/test/Unit/Config/RegistryStateTest.php similarity index 93% rename from test/Config/RegistryStateTest.php rename to test/Unit/Config/RegistryStateTest.php index ebabcbbd..89d33255 100644 --- a/test/Config/RegistryStateTest.php +++ b/test/Unit/Config/RegistryStateTest.php @@ -2,8 +2,10 @@ declare(strict_types=1); -namespace Horde\Core\Config; +namespace Horde\Core\Test\Unit\Config; +use Horde\Core\Config\RegistryState; +use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; /** @@ -14,7 +16,7 @@ * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 * @package Core */ -#[\PHPUnit\Framework\Attributes\CoversClass(RegistryState::class)] +#[CoversClass(RegistryState::class)] class RegistryStateTest extends TestCase { private RegistryState $state; diff --git a/test/Config/VhostTest.php b/test/Unit/Config/VhostTest.php similarity index 96% rename from test/Config/VhostTest.php rename to test/Unit/Config/VhostTest.php index 57e8d777..345a7c82 100644 --- a/test/Config/VhostTest.php +++ b/test/Unit/Config/VhostTest.php @@ -2,8 +2,10 @@ declare(strict_types=1); -namespace Horde\Core\Config; +namespace Horde\Core\Test\Unit\Config; +use Horde\Core\Config\Vhost; +use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; /** @@ -14,7 +16,7 @@ * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 * @package Core */ -#[\PHPUnit\Framework\Attributes\CoversClass(Vhost::class)] +#[CoversClass(Vhost::class)] class VhostTest extends TestCase { public function testExplicitHostname(): void diff --git a/test/ConfigStateTest.php b/test/Unit/ConfigStateTest.php similarity index 87% rename from test/ConfigStateTest.php rename to test/Unit/ConfigStateTest.php index e48abd3f..3655e74c 100644 --- a/test/ConfigStateTest.php +++ b/test/Unit/ConfigStateTest.php @@ -1,5 +1,7 @@ - * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 - * @coversNothing - */ +#[CoversNothing] class GroupTest extends TestCase { public function testMock() diff --git a/test/Factory/HordeLdapServiceFactoryTest.php b/test/Unit/Factory/HordeLdapServiceFactoryTest.php similarity index 99% rename from test/Factory/HordeLdapServiceFactoryTest.php rename to test/Unit/Factory/HordeLdapServiceFactoryTest.php index ef163862..39661a3a 100644 --- a/test/Factory/HordeLdapServiceFactoryTest.php +++ b/test/Unit/Factory/HordeLdapServiceFactoryTest.php @@ -14,7 +14,7 @@ * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 */ -namespace Horde\Core\Test\Factory; +namespace Horde\Core\Test\Unit\Factory; use Horde\Core\Factory\HordeLdapServiceFactory; use Horde\Core\Service\StandardHordeLdapService; diff --git a/test/Factory/KolabServerTest.php b/test/Unit/Factory/KolabServerTest.php similarity index 97% rename from test/Factory/KolabServerTest.php rename to test/Unit/Factory/KolabServerTest.php index d2fb1c37..44a4e0e5 100644 --- a/test/Factory/KolabServerTest.php +++ b/test/Unit/Factory/KolabServerTest.php @@ -1,9 +1,14 @@ - * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 - * @coversNothing - */ +#[CoversNothing] class KolabServerTest extends TestCase { public function setUp(): void diff --git a/test/Factory/KolabSessionTest.php b/test/Unit/Factory/KolabSessionTest.php similarity index 95% rename from test/Factory/KolabSessionTest.php rename to test/Unit/Factory/KolabSessionTest.php index bb0ccf2e..ca329f35 100644 --- a/test/Factory/KolabSessionTest.php +++ b/test/Unit/Factory/KolabSessionTest.php @@ -1,19 +1,6 @@ - * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 - */ - -namespace Horde\Core\Factory; - -use PHPUnit\Framework\TestCase; +declare(strict_types=1); /** * Test the Kolab_Session factory. @@ -27,8 +14,14 @@ * @package Core * @author Gunnar Wrobel * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 - * @coversNothing */ + +namespace Horde\Core\Test\Unit\Factory; + +use PHPUnit\Framework\Attributes\CoversNothing; +use PHPUnit\Framework\TestCase; + +#[CoversNothing] class KolabSessionTest extends TestCase { public function setUp(): void diff --git a/test/Factory/SessionHandlerFactoryTest.php b/test/Unit/Factory/SessionHandlerFactoryTest.php similarity index 99% rename from test/Factory/SessionHandlerFactoryTest.php rename to test/Unit/Factory/SessionHandlerFactoryTest.php index d604a984..f49d576a 100644 --- a/test/Factory/SessionHandlerFactoryTest.php +++ b/test/Unit/Factory/SessionHandlerFactoryTest.php @@ -13,7 +13,7 @@ * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 */ -namespace Horde\Core\Test\Factory; +namespace Horde\Core\Test\Unit\Factory; use Horde\Core\Config\ConfigLoader; use Horde\Core\Config\State; diff --git a/test/Middleware/AppFinderTest.php b/test/Unit/Middleware/AppFinderTest.php similarity index 98% rename from test/Middleware/AppFinderTest.php rename to test/Unit/Middleware/AppFinderTest.php index b5356288..8ad0f713 100644 --- a/test/Middleware/AppFinderTest.php +++ b/test/Unit/Middleware/AppFinderTest.php @@ -1,5 +1,7 @@ + * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 + */ + +namespace Horde\Core\Test\Unit\Service; + +use Horde\Core\Service\Exception\OAuthTokenNotFoundException; +use Horde\Core\Service\NullOAuthTokenService; +use Horde\Core\Service\OAuthTokenService; +use Horde\Oauth\Client\TokenSet; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\TestCase; + +#[CoversClass(NullOAuthTokenService::class)] +final class NullOAuthTokenServiceTest extends TestCase +{ + private NullOAuthTokenService $service; + + protected function setUp(): void + { + $this->service = new NullOAuthTokenService(); + } + + public function testImplementsInterface(): void + { + self::assertInstanceOf(OAuthTokenService::class, $this->service); + } + + public function testHasTokensReturnsFalse(): void + { + self::assertFalse($this->service->hasTokens('user1', 'google')); + } + + public function testStoreIsSilentlyAccepted(): void + { + $tokens = new TokenSet('access-token', 'Bearer'); + $this->service->store('user1', 'google', $tokens); + self::assertFalse($this->service->hasTokens('user1', 'google')); + } + + public function testRemoveIsSilentlyAccepted(): void + { + $this->service->remove('user1', 'google'); + self::assertFalse($this->service->hasTokens('user1', 'google')); + } + + public function testGetAccessTokenThrows(): void + { + $this->expectException(OAuthTokenNotFoundException::class); + $this->service->getAccessToken('user1', 'google'); + } + + public function testGetTokenSetThrows(): void + { + $this->expectException(OAuthTokenNotFoundException::class); + $this->service->getTokenSet('user1', 'google'); + } +} diff --git a/test/Session/HordeSessionFactoryTest.php b/test/Unit/Session/HordeSessionFactoryTest.php similarity index 99% rename from test/Session/HordeSessionFactoryTest.php rename to test/Unit/Session/HordeSessionFactoryTest.php index ddbc8a87..f8762593 100644 --- a/test/Session/HordeSessionFactoryTest.php +++ b/test/Unit/Session/HordeSessionFactoryTest.php @@ -9,7 +9,7 @@ * did not receive this file, see http://www.horde.org/licenses/lgpl21. */ -namespace Horde\Core\Test\Session; +namespace Horde\Core\Test\Unit\Session; use Horde\Core\Session\HordeSession; use Horde\Core\Session\HordeSessionFactory; diff --git a/test/Session/HordeSessionTest.php b/test/Unit/Session/HordeSessionTest.php similarity index 99% rename from test/Session/HordeSessionTest.php rename to test/Unit/Session/HordeSessionTest.php index 6efdbac4..f5b58d24 100644 --- a/test/Session/HordeSessionTest.php +++ b/test/Unit/Session/HordeSessionTest.php @@ -9,7 +9,7 @@ * did not receive this file, see http://www.horde.org/licenses/lgpl21. */ -namespace Horde\Core\Test\Session; +namespace Horde\Core\Test\Unit\Session; use Horde\Core\Session\EncryptedValuesInterface; use Horde\Core\Session\HordeSession; diff --git a/test/SmartmobileUrlTest.php b/test/Unit/SmartmobileUrlTest.php similarity index 90% rename from test/SmartmobileUrlTest.php rename to test/Unit/SmartmobileUrlTest.php index 2ef7976c..06fee194 100644 --- a/test/SmartmobileUrlTest.php +++ b/test/Unit/SmartmobileUrlTest.php @@ -1,8 +1,11 @@ createStub(Horde_Registry::class); // Use stubs for logger and injector - $logger = $this->createStub(Psr\Log\LoggerInterface::class); + $logger = $this->createStub(LoggerInterface::class); $GLOBALS['injector'] = $this->createStub(Horde_Injector::class); $GLOBALS['injector']->method('get')->willReturn($logger); diff --git a/test/UrlTest.php b/test/Unit/UrlTest.php similarity index 99% rename from test/UrlTest.php rename to test/Unit/UrlTest.php index f57cfa42..c79c8cab 100644 --- a/test/UrlTest.php +++ b/test/Unit/UrlTest.php @@ -1,8 +1,11 @@ load('turba'); - $detector = new StrftimeDetector(); - $findings = $detector->scan($prefsState, ['date_format', 'time_format']); - - if (empty($findings)) { - echo "✅ No strftime patterns found\n"; - } else { - echo '⚠️ Found ' . count($findings) . " strftime pattern(s):\n\n"; - foreach ($findings as $finding) { - echo ' • ' . $finding->format() . "\n"; - } - } -} catch (Exception $e) { - echo 'Error loading prefs: ' . $e->getMessage() . "\n"; -} - -// Example 2: Scan raw array -echo "\n\n=== Example 2: Scan raw array ===\n\n"; - -$prefsArray = [ - 'date_format' => [ - 'value' => '%Y-%m-%d', - 'type' => 'enum', - 'enum' => [ - '%Y-%m-%d' => 'ISO 8601', - '%d.%m.%Y' => 'European', - '%m/%d/%Y' => 'US', - ], - 'desc' => 'Date format', - ], - 'time_format' => [ - 'value' => '%H:%M:%S', - 'type' => 'text', - ], - 'clean_pref' => [ - 'value' => 'no patterns here', - ], -]; - -$detector = new StrftimeDetector(); -$findings = $detector->scanArray($prefsArray); - -echo 'Found ' . count($findings) . " strftime pattern(s):\n\n"; -foreach ($findings as $finding) { - echo sprintf( - " • Pref: %s\n" - . " Field: %s\n" - . " Strftime: %s\n" - . " ICU: %s\n" - . " Confidence: %s\n\n", - $finding->pref, - $finding->field, - $finding->strftime, - $finding->getIcuString(), - $finding->confidence - ); -} - -// Example 3: Filter by confidence level -echo "\n=== Example 3: Filter by confidence ===\n\n"; - -$highConfidence = array_filter( - $findings, - fn($f) => $f->confidence === StrftimeFinding::CONFIDENCE_HIGH -); - -echo 'High confidence findings: ' . count($highConfidence) . "\n"; -foreach ($highConfidence as $finding) { - echo ' • ' . $finding->format() . "\n"; -} - -// Example 4: JSON output -echo "\n\n=== Example 4: JSON output ===\n\n"; - -echo json_encode([ - 'status' => 'success', - 'count' => count($findings), - 'findings' => $findings, -], JSON_PRETTY_PRINT) . "\n";