diff --git a/build/psalm-baseline.xml b/build/psalm-baseline.xml index 07df0637d57f4..1d6d14a503f54 100644 --- a/build/psalm-baseline.xml +++ b/build/psalm-baseline.xml @@ -2980,9 +2980,6 @@ - - - @@ -3301,11 +3298,6 @@ - - - - - @@ -3314,11 +3306,6 @@ - - - fastCache[$app][$key] ?? $default]]> - - @@ -4015,9 +4002,6 @@ - - - @@ -4394,9 +4378,6 @@ - - - diff --git a/config/config.sample.php b/config/config.sample.php index 37871669309e9..ea8766b970b7b 100644 --- a/config/config.sample.php +++ b/config/config.sample.php @@ -1792,6 +1792,15 @@ */ 'cache_chunk_gc_ttl' => 60*60*24, +/** + * Enable caching of the app config values. + * If enabled the app config will be cached locally for a short TTL, + * reducing database load significatly on larger setups. + * + * Defaults to ``true`` + */ +'cache_app_config' => true, + /** * Using Object Store with Nextcloud */ diff --git a/core/Command/Encryption/Enable.php b/core/Command/Encryption/Enable.php index 2c476185692c5..02045fa1a4ba6 100644 --- a/core/Command/Encryption/Enable.php +++ b/core/Command/Encryption/Enable.php @@ -42,8 +42,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int $output->writeln('No encryption module is loaded'); return 1; } - $defaultModule = $this->config->getAppValue('core', 'default_encryption_module', null); - if ($defaultModule === null) { + $defaultModule = $this->config->getAppValue('core', 'default_encryption_module'); + if ($defaultModule === '') { $output->writeln('No default module is set'); return 1; } diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts index ad486a8a8f7c5..b09a165451f09 100644 --- a/cypress/support/commands.ts +++ b/cypress/support/commands.ts @@ -246,3 +246,16 @@ Cypress.Commands.add('userFileExists', (user: string, path: string) => { return cy.runCommand(`stat --printf="%s" "data/${user}/files/${path}"`, { failOnNonZeroExit: true }) .then((exec) => Number.parseInt(exec.stdout || '0')) }) + +Cypress.Commands.add('runOccCommand', (command: string, options?: Partial) => { + return cy.runCommand(`php ./occ ${command}`, options) + .then((context) => + // OCC cannot clear the APCu cache + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait( + command.startsWith('app:') || command.startsWith('config:') + ? 3000 // clear APCu cache + : 0, + ).then(() => context), + ) +}) diff --git a/lib/private/AllConfig.php b/lib/private/AllConfig.php index c80ee52eb0dc6..c4a6989df1317 100644 --- a/lib/private/AllConfig.php +++ b/lib/private/AllConfig.php @@ -195,7 +195,7 @@ public function setAppValue($appName, $key, $value) { * @deprecated 29.0.0 Use {@see IAppConfig} directly */ public function getAppValue($appName, $key, $default = '') { - return \OC::$server->get(AppConfig::class)->getValue($appName, $key, $default); + return \OC::$server->get(AppConfig::class)->getValue($appName, $key, $default) ?? $default; } /** diff --git a/lib/private/AppConfig.php b/lib/private/AppConfig.php index cef612536d620..2fbc7fe38125f 100644 --- a/lib/private/AppConfig.php +++ b/lib/private/AppConfig.php @@ -14,8 +14,8 @@ use OC\AppFramework\Bootstrap\Coordinator; use OC\Config\ConfigManager; use OC\Config\PresetManager; +use OC\Memcache\Factory as CacheFactory; use OCP\Config\Lexicon\Entry; -use OCP\Config\Lexicon\ILexicon; use OCP\Config\Lexicon\Strictness; use OCP\Config\ValueType; use OCP\DB\Exception as DBException; @@ -24,6 +24,8 @@ use OCP\Exceptions\AppConfigTypeConflictException; use OCP\Exceptions\AppConfigUnknownKeyException; use OCP\IAppConfig; +use OCP\ICache; +use OCP\ICacheFactory; use OCP\IConfig; use OCP\IDBConnection; use OCP\Security\ICrypto; @@ -53,10 +55,12 @@ class AppConfig implements IAppConfig { private const KEY_MAX_LENGTH = 64; private const ENCRYPTION_PREFIX = '$AppConfigEncryption$'; private const ENCRYPTION_PREFIX_LENGTH = 21; // strlen(self::ENCRYPTION_PREFIX) + private const LOCAL_CACHE_KEY = 'OC\\AppConfig'; + private const LOCAL_CACHE_TTL = 3; - /** @var array> ['app_id' => ['config_key' => 'config_value']] */ + /** @var array> ['app_id' => ['config_key' => 'config_value']] */ private array $fastCache = []; // cache for normal config keys - /** @var array> ['app_id' => ['config_key' => 'config_value']] */ + /** @var array> ['app_id' => ['config_key' => 'config_value']] */ private array $lazyCache = []; // cache for lazy config keys /** @var array> ['app_id' => ['config_key' => bitflag]] */ private array $valueTypes = []; // type for all config values @@ -67,6 +71,7 @@ class AppConfig implements IAppConfig { private bool $ignoreLexiconAliases = false; /** @var ?array */ private ?array $appVersionsCache = null; + private ?ICache $localCache = null; public function __construct( protected IDBConnection $connection, @@ -75,7 +80,13 @@ public function __construct( private readonly PresetManager $presetManager, protected LoggerInterface $logger, protected ICrypto $crypto, + readonly CacheFactory $cacheFactory, ) { + if ($config->getSystemValueBool('cache_app_config', true) && $cacheFactory->isLocalCacheAvailable()) { + $cacheFactory->withServerVersionPrefix(function (ICacheFactory $factory) { + $this->localCache = $factory->createLocal(); + }); + } } /** @@ -85,7 +96,7 @@ public function __construct( * @since 7.0.0 */ public function getApps(): array { - $this->loadConfigAll(); + $this->loadConfig(lazy: true); $apps = array_merge(array_keys($this->fastCache), array_keys($this->lazyCache)); sort($apps); @@ -103,7 +114,7 @@ public function getApps(): array { */ public function getKeys(string $app): array { $this->assertParams($app); - $this->loadConfigAll($app); + $this->loadConfig($app, true); $keys = array_merge(array_keys($this->fastCache[$app] ?? []), array_keys($this->lazyCache[$app] ?? [])); sort($keys); @@ -149,19 +160,16 @@ public function searchKeys(string $app, string $prefix = '', bool $lazy = false) */ public function hasKey(string $app, string $key, ?bool $lazy = false): bool { $this->assertParams($app, $key); - $this->loadConfig($app, $lazy); + $this->loadConfig($app, $lazy ?? true); $this->matchAndApplyLexiconDefinition($app, $key); + $hasLazy = isset($this->lazyCache[$app][$key]); + $hasFast = isset($this->fastCache[$app][$key]); if ($lazy === null) { - $appCache = $this->getAllValues($app); - return isset($appCache[$key]); - } - - if ($lazy) { - return isset($this->lazyCache[$app][$key]); + return $hasLazy || $hasFast; + } else { + return $lazy ? $hasLazy : $hasFast; } - - return isset($this->fastCache[$app][$key]); } /** @@ -175,7 +183,7 @@ public function hasKey(string $app, string $key, ?bool $lazy = false): bool { */ public function isSensitive(string $app, string $key, ?bool $lazy = false): bool { $this->assertParams($app, $key); - $this->loadConfig(null, $lazy); + $this->loadConfig(null, $lazy ?? true); $this->matchAndApplyLexiconDefinition($app, $key); if (!isset($this->valueTypes[$app][$key])) { @@ -227,7 +235,7 @@ public function isLazy(string $app, string $key): bool { public function getAllValues(string $app, string $prefix = '', bool $filtered = false): array { $this->assertParams($app, $prefix); // if we want to filter values, we need to get sensitivity - $this->loadConfigAll($app); + $this->loadConfig($app, true); // array_merge() will remove numeric keys (here config keys), so addition arrays instead $values = $this->formatAppValues($app, ($this->fastCache[$app] ?? []) + ($this->lazyCache[$app] ?? [])); $values = array_filter( @@ -479,7 +487,7 @@ private function getTypedValue( return $default; } - $this->loadConfig($app, $lazy); + $this->loadConfig($app, $lazy ?? true); /** * We ignore check if mixed type is requested. @@ -551,7 +559,7 @@ public function getValueType(string $app, string $key, ?bool $lazy = null): int } $this->assertParams($app, $key); - $this->loadConfig($app, $lazy); + $this->loadConfig($app, $lazy ?? true); if (!isset($this->valueTypes[$app][$key])) { throw new AppConfigUnknownKeyException('unknown config key'); @@ -788,7 +796,7 @@ private function setTypedValue( if (!$this->matchAndApplyLexiconDefinition($app, $key, $lazy, $type)) { return false; // returns false as database is not updated } - $this->loadConfig(null, $lazy); + $this->loadConfig(null, $lazy ?? true); $sensitive = $this->isTyped(self::VALUE_SENSITIVE, $type); $inserted = $refreshCache = false; @@ -803,7 +811,7 @@ private function setTypedValue( * no update if key is already known with set lazy status and value is * not different, unless sensitivity is switched from false to true. */ - if ($origValue === $this->getTypedValue($app, $key, $value, $lazy, $type) + if ($origValue === $this->getTypedValue($app, $key, $value, $lazy ?? true, $type) && (!$sensitive || $this->isSensitive($app, $key, $lazy))) { return false; } @@ -835,7 +843,7 @@ private function setTypedValue( if (!$inserted) { $currType = $this->valueTypes[$app][$key] ?? 0; if ($currType === 0) { // this might happen when switching lazy loading status - $this->loadConfigAll(); + $this->loadConfig(lazy: true); $currType = $this->valueTypes[$app][$key] ?? 0; } @@ -856,7 +864,7 @@ private function setTypedValue( && ($type | self::VALUE_SENSITIVE) !== ($currType | self::VALUE_SENSITIVE)) { try { $currType = $this->convertTypeToString($currType); - $type = $this->convertTypeToString($type); + $this->convertTypeToString($type); } catch (AppConfigIncorrectTypeException) { // can be ignored, this was just needed for a better exception message. } @@ -895,6 +903,7 @@ private function setTypedValue( $this->fastCache[$app][$key] = $value; } $this->valueTypes[$app][$key] = $type; + $this->clearLocalCache(); return true; } @@ -916,7 +925,7 @@ private function setTypedValue( */ public function updateType(string $app, string $key, int $type = self::VALUE_MIXED): bool { $this->assertParams($app, $key); - $this->loadConfigAll(); + $this->loadConfig(lazy: true); $this->matchAndApplyLexiconDefinition($app, $key); $this->isLazy($app, $key); // confirm key exists @@ -959,7 +968,7 @@ public function updateType(string $app, string $key, int $type = self::VALUE_MIX */ public function updateSensitive(string $app, string $key, bool $sensitive): bool { $this->assertParams($app, $key); - $this->loadConfigAll(); + $this->loadConfig(lazy: true); $this->matchAndApplyLexiconDefinition($app, $key); try { @@ -1019,7 +1028,7 @@ public function updateSensitive(string $app, string $key, bool $sensitive): bool */ public function updateLazy(string $app, string $key, bool $lazy): bool { $this->assertParams($app, $key); - $this->loadConfigAll(); + $this->loadConfig(lazy: true); $this->matchAndApplyLexiconDefinition($app, $key); try { @@ -1055,7 +1064,7 @@ public function updateLazy(string $app, string $key, bool $lazy): bool { */ public function getDetails(string $app, string $key): array { $this->assertParams($app, $key); - $this->loadConfigAll(); + $this->loadConfig(lazy: true); $this->matchAndApplyLexiconDefinition($app, $key); $lazy = $this->isLazy($app, $key); @@ -1198,6 +1207,7 @@ public function deleteKey(string $app, string $key): void { unset($this->lazyCache[$app][$key]); unset($this->fastCache[$app][$key]); unset($this->valueTypes[$app][$key]); + $this->clearLocalCache(); } /** @@ -1227,12 +1237,13 @@ public function deleteApp(string $app): void { public function clearCache(bool $reload = false): void { $this->lazyLoaded = $this->fastLoaded = false; $this->lazyCache = $this->fastCache = $this->valueTypes = $this->configLexiconDetails = []; + $this->localCache?->remove(self::LOCAL_CACHE_KEY); if (!$reload) { return; } - $this->loadConfigAll(); + $this->loadConfig(lazy: true); } @@ -1293,35 +1304,49 @@ private function assertParams(string $app = '', string $configKey = '', bool $al } } - private function loadConfigAll(?string $app = null): void { - $this->loadConfig($app, null); - } - /** * Load normal config or config set as lazy loaded * - * @param bool|null $lazy set to TRUE to load config set as lazy loaded, set to NULL to load all config + * @param bool $lazy set to TRUE to also load config values set as lazy loaded */ - private function loadConfig(?string $app = null, ?bool $lazy = false): void { + private function loadConfig(?string $app = null, bool $lazy = false): void { if ($this->isLoaded($lazy)) { return; } // if lazy is null or true, we debug log - if (($lazy ?? true) !== false && $app !== null) { + if ($lazy === true && $app !== null) { $exception = new \RuntimeException('The loading of lazy AppConfig values have been triggered by app "' . $app . '"'); $this->logger->debug($exception->getMessage(), ['exception' => $exception, 'app' => $app]); } - $qb = $this->connection->getQueryBuilder(); - $qb->from('appconfig'); + $loadLazyOnly = $lazy && $this->isLoaded(); - // we only need value from lazy when loadConfig does not specify it - $qb->select('appid', 'configkey', 'configvalue', 'type'); + /** @var array */ + $cacheContent = $this->localCache?->get(self::LOCAL_CACHE_KEY) ?? []; + $includesLazyValues = !empty($cacheContent) && !empty($cacheContent['lazyCache']); + if (!empty($cacheContent) && (!$lazy || $includesLazyValues)) { + $this->valueTypes = $cacheContent['valueTypes']; + $this->fastCache = $cacheContent['fastCache']; + $this->fastLoaded = !empty($this->fastCache); + if ($includesLazyValues) { + $this->lazyCache = $cacheContent['lazyCache']; + $this->lazyLoaded = !empty($this->lazyCache); + } + return; + } - if ($lazy !== null) { - $qb->where($qb->expr()->eq('lazy', $qb->createNamedParameter($lazy ? 1 : 0, IQueryBuilder::PARAM_INT))); + // Otherwise no cache available and we need to fetch from database + $qb = $this->connection->getQueryBuilder(); + $qb->from('appconfig') + ->select('appid', 'configkey', 'configvalue', 'type'); + + if ($lazy === false) { + $qb->where($qb->expr()->eq('lazy', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT))); } else { + if ($loadLazyOnly) { + $qb->where($qb->expr()->eq('lazy', $qb->createNamedParameter(1, IQueryBuilder::PARAM_INT))); + } $qb->addSelect('lazy'); } @@ -1329,56 +1354,34 @@ private function loadConfig(?string $app = null, ?bool $lazy = false): void { $rows = $result->fetchAll(); foreach ($rows as $row) { // most of the time, 'lazy' is not in the select because its value is already known - if (($row['lazy'] ?? ($lazy ?? 0) ? 1 : 0) === 1) { + if ($lazy && ((int)$row['lazy']) === 1) { $this->lazyCache[$row['appid']][$row['configkey']] = $row['configvalue'] ?? ''; } else { $this->fastCache[$row['appid']][$row['configkey']] = $row['configvalue'] ?? ''; } $this->valueTypes[$row['appid']][$row['configkey']] = (int)($row['type'] ?? 0); } - $result->closeCursor(); - $this->setAsLoaded($lazy); - } - /** - * if $lazy is: - * - false: will returns true if fast config is loaded - * - true : will returns true if lazy config is loaded - * - null : will returns true if both config are loaded - * - * @param bool $lazy - * - * @return bool - */ - private function isLoaded(?bool $lazy): bool { - if ($lazy === null) { - return $this->lazyLoaded && $this->fastLoaded; - } + $result->closeCursor(); + $this->localCache?->set( + self::LOCAL_CACHE_KEY, + [ + 'fastCache' => $this->fastCache, + 'lazyCache' => $this->lazyCache, + 'valueTypes' => $this->valueTypes, + ], + self::LOCAL_CACHE_TTL, + ); - return $lazy ? $this->lazyLoaded : $this->fastLoaded; + $this->fastLoaded = true; + $this->lazyLoaded = $lazy; } /** - * if $lazy is: - * - false: set fast config as loaded - * - true : set lazy config as loaded - * - null : set both config as loaded - * - * @param bool $lazy + * @param bool $lazy - If set to true then also check if lazy values are loaded */ - private function setAsLoaded(?bool $lazy): void { - if ($lazy === null) { - $this->fastLoaded = true; - $this->lazyLoaded = true; - - return; - } - - if ($lazy) { - $this->lazyLoaded = true; - } else { - $this->fastLoaded = true; - } + private function isLoaded(bool $lazy = false): bool { + return $this->fastLoaded && (!$lazy || $this->lazyLoaded); } /** @@ -1386,7 +1389,7 @@ private function setAsLoaded(?bool $lazy): void { * * @param string $app app * @param string $key key - * @param string $default = null, default value if the key does not exist + * @param string $default - Default value if the key does not exist * * @return string the value or $default * @deprecated 29.0.0 use getValue*() @@ -1394,7 +1397,7 @@ private function setAsLoaded(?bool $lazy): void { * This function gets a value from the appconfig table. If the key does * not exist the default value will be returned */ - public function getValue($app, $key, $default = null) { + public function getValue($app, $key, $default = '') { $this->loadConfig($app); $this->matchAndApplyLexiconDefinition($app, $key); @@ -1421,7 +1424,7 @@ public function setValue($app, $key, $value) { * or enabled (lazy=lazy-2) * * this solution would remove the loading of config values from disabled app - * unless calling the method {@see loadConfigAll()} + * unless calling the method. */ return $this->setTypedValue($app, $key, (string)$value, false, self::VALUE_MIXED); } @@ -1733,7 +1736,7 @@ private function matchAndApplyLexiconDefinition( * * @return bool TRUE if conflict can be fully ignored, FALSE if action should be not performed * @throws AppConfigUnknownKeyException if strictness implies exception - * @see ILexicon::getStrictness() + * @see \OCP\Config\Lexicon\ILexicon::getStrictness() */ private function applyLexiconStrictness( ?Strictness $strictness, @@ -1772,8 +1775,9 @@ public function getConfigDetailsFromLexicon(string $appId): array { $configLexicon = $bootstrapCoordinator->getRegistrationContext()?->getConfigLexicon($appId); foreach ($configLexicon?->getAppConfigs() ?? [] as $configEntry) { $entries[$configEntry->getKey()] = $configEntry; - if ($configEntry->getRename() !== null) { - $aliases[$configEntry->getRename()] = $configEntry->getKey(); + $newName = $configEntry->getRename(); + if ($newName !== null) { + $aliases[$newName] = $configEntry->getKey(); } } @@ -1819,4 +1823,8 @@ public function getAppInstalledVersions(bool $onlyEnabled = false): array { } return $this->appVersionsCache; } + + private function clearLocalCache(): void { + $this->localCache?->remove(self::LOCAL_CACHE_KEY); + } } diff --git a/lib/private/Installer.php b/lib/private/Installer.php index 91d20a129ae55..fd737d286ad81 100644 --- a/lib/private/Installer.php +++ b/lib/private/Installer.php @@ -546,7 +546,7 @@ public static function installShippedApps(bool $softErrors = false, ?IOutput $ou while (false !== ($filename = readdir($dir))) { if ($filename[0] !== '.' and is_dir($app_dir['path'] . "/$filename")) { if (file_exists($app_dir['path'] . "/$filename/appinfo/info.xml")) { - if ($config->getAppValue($filename, 'installed_version', null) === null) { + if ($config->getAppValue($filename, 'installed_version') === '') { $enabled = $appManager->isDefaultEnabled($filename); if (($enabled || in_array($filename, $appManager->getAlwaysEnabledApps())) && $config->getAppValue($filename, 'enabled') !== 'no') { diff --git a/lib/private/Memcache/Factory.php b/lib/private/Memcache/Factory.php index b54189937fc12..2a3fabc89f18e 100644 --- a/lib/private/Memcache/Factory.php +++ b/lib/private/Memcache/Factory.php @@ -7,72 +7,65 @@ */ namespace OC\Memcache; -use Closure; +use OC\SystemConfig; use OCP\Cache\CappedMemoryCache; +use OCP\IAppConfig; use OCP\ICache; use OCP\ICacheFactory; use OCP\IMemcache; use OCP\Profiler\IProfiler; +use OCP\ServerVersion; use Psr\Log\LoggerInterface; class Factory implements ICacheFactory { public const NULL_CACHE = NullCache::class; - private ?string $globalPrefix = null; - - private LoggerInterface $logger; - + protected ?string $globalPrefix = null; /** - * @var ?class-string $localCacheClass + * @var class-string $localCacheClass */ - private ?string $localCacheClass; + protected string $localCacheClass; /** - * @var ?class-string $distributedCacheClass + * @var class-string $distributedCacheClass */ - private ?string $distributedCacheClass; + protected string $distributedCacheClass; /** - * @var ?class-string $lockingCacheClass + * @var class-string $lockingCacheClass */ - private ?string $lockingCacheClass; - - private string $logFile; - - private IProfiler $profiler; + protected string $lockingCacheClass; /** - * @param Closure $globalPrefixClosure - * @param LoggerInterface $logger * @param ?class-string $localCacheClass * @param ?class-string $distributedCacheClass * @param ?class-string $lockingCacheClass - * @param string $logFile */ public function __construct( - private Closure $globalPrefixClosure, - LoggerInterface $logger, - IProfiler $profiler, + protected LoggerInterface $logger, + protected IProfiler $profiler, + protected ServerVersion $serverVersion, ?string $localCacheClass = null, ?string $distributedCacheClass = null, ?string $lockingCacheClass = null, - string $logFile = '', + protected string $logFile = '', ) { - $this->logFile = $logFile; - if (!$localCacheClass) { $localCacheClass = self::NULL_CACHE; } $localCacheClass = ltrim($localCacheClass, '\\'); + if (!$distributedCacheClass) { $distributedCacheClass = $localCacheClass; } - $distributedCacheClass = ltrim($distributedCacheClass, '\\'); $missingCacheMessage = 'Memcache {class} not available for {use} cache'; $missingCacheHint = 'Is the matching PHP module installed and enabled?'; - if (!class_exists($localCacheClass) || !$localCacheClass::isAvailable()) { + if (!class_exists($localCacheClass) + || !is_a($localCacheClass, ICache::class, true) + || !$localCacheClass::isAvailable() + ) { if (\OC::$CLI && !defined('PHPUNIT_RUN') && $localCacheClass === APCu::class) { // CLI should not fail if APCu is not available but fallback to NullCache. // This can be the case if APCu is used without apc.enable_cli=1. @@ -84,7 +77,11 @@ public function __construct( ]), $missingCacheHint); } } - if (!class_exists($distributedCacheClass) || !$distributedCacheClass::isAvailable()) { + + if (!class_exists($distributedCacheClass) + || !is_a($distributedCacheClass, ICache::class, true) + || !$distributedCacheClass::isAvailable() + ) { if (\OC::$CLI && !defined('PHPUNIT_RUN') && $distributedCacheClass === APCu::class) { // CLI should not fail if APCu is not available but fallback to NullCache. // This can be the case if APCu is used without apc.enable_cli=1. @@ -96,25 +93,51 @@ public function __construct( ]), $missingCacheHint); } } - if (!($lockingCacheClass && class_exists($lockingCacheClass) && $lockingCacheClass::isAvailable())) { + + if (!$lockingCacheClass + || !class_exists($lockingCacheClass) + || !is_a($lockingCacheClass, IMemcache::class, true) + || !$lockingCacheClass::isAvailable() + ) { // don't fall back since the fallback might not be suitable for storing lock $lockingCacheClass = self::NULL_CACHE; } + /** @var class-string */ $lockingCacheClass = ltrim($lockingCacheClass, '\\'); $this->localCacheClass = $localCacheClass; $this->distributedCacheClass = $distributedCacheClass; $this->lockingCacheClass = $lockingCacheClass; - $this->profiler = $profiler; } - private function getGlobalPrefix(): ?string { - if (is_null($this->globalPrefix)) { - $this->globalPrefix = ($this->globalPrefixClosure)(); + protected function getGlobalPrefix(): string { + if ($this->globalPrefix === null) { + $config = \OCP\Server::get(SystemConfig::class); + $versions = []; + if ($config->getValue('installed', false)) { + $appConfig = \OCP\Server::get(IAppConfig::class); + $versions = $appConfig->getAppInstalledVersions(); + } + $versions['core'] = implode('.', $this->serverVersion->getVersion()); + $this->globalPrefix = hash('xxh128', implode(',', $versions)); } return $this->globalPrefix; } + /** + * Override the global prefix for a specific closure. + * This should only be used internally for bootstrapping purpose! + * + * @param string $globalPrefix - The prefix to use during the closure execution + * @param \Closure $closure - The closure with the cache factory as the first parameter + */ + public function withServerVersionPrefix(\Closure $closure): void { + $backupPrefix = $this->globalPrefix; + $this->globalPrefix = hash('xxh128', implode('.', $this->serverVersion->getVersion())); + $closure($this); + $this->globalPrefix = $backupPrefix; + } + /** * create a cache instance for storing locks * @@ -122,22 +145,17 @@ private function getGlobalPrefix(): ?string { * @return IMemcache */ public function createLocking(string $prefix = ''): IMemcache { - $globalPrefix = $this->getGlobalPrefix(); - if (is_null($globalPrefix)) { - return new ArrayCache($prefix); - } - - assert($this->lockingCacheClass !== null); - $cache = new $this->lockingCacheClass($globalPrefix . '/' . $prefix); - if ($this->lockingCacheClass === Redis::class && $this->profiler->isEnabled()) { - // We only support the profiler with Redis - $cache = new ProfilerWrapperCache($cache, 'Locking'); - $this->profiler->add($cache); - } + $cache = new $this->lockingCacheClass($this->getGlobalPrefix() . '/' . $prefix); + if ($this->lockingCacheClass === Redis::class) { + if ($this->profiler->isEnabled()) { + // We only support the profiler with Redis + $cache = new ProfilerWrapperCache($cache, 'Locking'); + $this->profiler->add($cache); + } - if ($this->lockingCacheClass === Redis::class - && $this->logFile !== '' && is_writable(dirname($this->logFile)) && (!file_exists($this->logFile) || is_writable($this->logFile))) { - $cache = new LoggerWrapperCache($cache, $this->logFile); + if ($this->logFile !== '' && is_writable(dirname($this->logFile)) && (!file_exists($this->logFile) || is_writable($this->logFile))) { + $cache = new LoggerWrapperCache($cache, $this->logFile); + } } return $cache; } @@ -149,22 +167,17 @@ public function createLocking(string $prefix = ''): IMemcache { * @return ICache */ public function createDistributed(string $prefix = ''): ICache { - $globalPrefix = $this->getGlobalPrefix(); - if (is_null($globalPrefix)) { - return new ArrayCache($prefix); - } - - assert($this->distributedCacheClass !== null); - $cache = new $this->distributedCacheClass($globalPrefix . '/' . $prefix); - if ($this->distributedCacheClass === Redis::class && $this->profiler->isEnabled()) { - // We only support the profiler with Redis - $cache = new ProfilerWrapperCache($cache, 'Distributed'); - $this->profiler->add($cache); - } + $cache = new $this->distributedCacheClass($this->getGlobalPrefix() . '/' . $prefix); + if ($this->distributedCacheClass === Redis::class) { + if ($this->profiler->isEnabled()) { + // We only support the profiler with Redis + $cache = new ProfilerWrapperCache($cache, 'Distributed'); + $this->profiler->add($cache); + } - if ($this->distributedCacheClass === Redis::class && $this->logFile !== '' - && is_writable(dirname($this->logFile)) && (!file_exists($this->logFile) || is_writable($this->logFile))) { - $cache = new LoggerWrapperCache($cache, $this->logFile); + if ($this->logFile !== '' && is_writable(dirname($this->logFile)) && (!file_exists($this->logFile) || is_writable($this->logFile))) { + $cache = new LoggerWrapperCache($cache, $this->logFile); + } } return $cache; } @@ -176,22 +189,17 @@ public function createDistributed(string $prefix = ''): ICache { * @return ICache */ public function createLocal(string $prefix = ''): ICache { - $globalPrefix = $this->getGlobalPrefix(); - if (is_null($globalPrefix)) { - return new ArrayCache($prefix); - } - - assert($this->localCacheClass !== null); - $cache = new $this->localCacheClass($globalPrefix . '/' . $prefix); - if ($this->localCacheClass === Redis::class && $this->profiler->isEnabled()) { - // We only support the profiler with Redis - $cache = new ProfilerWrapperCache($cache, 'Local'); - $this->profiler->add($cache); - } + $cache = new $this->localCacheClass($this->getGlobalPrefix() . '/' . $prefix); + if ($this->localCacheClass === Redis::class) { + if ($this->profiler->isEnabled()) { + // We only support the profiler with Redis + $cache = new ProfilerWrapperCache($cache, 'Local'); + $this->profiler->add($cache); + } - if ($this->localCacheClass === Redis::class && $this->logFile !== '' - && is_writable(dirname($this->logFile)) && (!file_exists($this->logFile) || is_writable($this->logFile))) { - $cache = new LoggerWrapperCache($cache, $this->logFile); + if ($this->logFile !== '' && is_writable(dirname($this->logFile)) && (!file_exists($this->logFile) || is_writable($this->logFile))) { + $cache = new LoggerWrapperCache($cache, $this->logFile); + } } return $cache; } @@ -217,4 +225,11 @@ public function createInMemory(int $capacity = 512): ICache { public function isLocalCacheAvailable(): bool { return $this->localCacheClass !== self::NULL_CACHE; } + + public function clearAll(): void { + $this->createLocal()->clear(); + $this->createDistributed()->clear(); + $this->createLocking()->clear(); + $this->createInMemory()->clear(); + } } diff --git a/lib/private/Server.php b/lib/private/Server.php index 22cd13438b847..49bda19a738bc 100644 --- a/lib/private/Server.php +++ b/lib/private/Server.php @@ -585,62 +585,37 @@ public function __construct($webRoot, \OC\Config $config) { $this->registerAlias(IURLGenerator::class, URLGenerator::class); - $this->registerService(ICache::class, function ($c) { - return new Cache\File(); - }); - + $this->registerAlias(ICache::class, Cache\File::class); $this->registerService(Factory::class, function (Server $c) { $profiler = $c->get(IProfiler::class); - $arrayCacheFactory = new \OC\Memcache\Factory(fn () => '', $c->get(LoggerInterface::class), - $profiler, - ArrayCache::class, - ArrayCache::class, - ArrayCache::class - ); + $logger = $c->get(LoggerInterface::class); + $serverVersion = $c->get(ServerVersion::class); /** @var SystemConfig $config */ $config = $c->get(SystemConfig::class); - /** @var ServerVersion $serverVersion */ - $serverVersion = $c->get(ServerVersion::class); - - if ($config->getValue('installed', false) && !(defined('PHPUNIT_RUN') && PHPUNIT_RUN)) { - $logQuery = $config->getValue('log_query'); - $prefixClosure = function () use ($logQuery, $serverVersion): ?string { - if (!$logQuery) { - try { - $v = \OCP\Server::get(IAppConfig::class)->getAppInstalledVersions(true); - } catch (\Doctrine\DBAL\Exception $e) { - // Database service probably unavailable - // Probably related to https://github.com/nextcloud/server/issues/37424 - return null; - } - } else { - // If the log_query is enabled, we can not get the app versions - // as that does a query, which will be logged and the logging - // depends on redis and here we are back again in the same function. - $v = [ - 'log_query' => 'enabled', - ]; - } - $v['core'] = implode(',', $serverVersion->getVersion()); - $version = implode(',', array_keys($v)) . implode(',', $v); - $instanceId = \OC_Util::getInstanceId(); - $path = \OC::$SERVERROOT; - return md5($instanceId . '-' . $version . '-' . $path); - }; - return new \OC\Memcache\Factory($prefixClosure, - $c->get(LoggerInterface::class), + if (!$config->getValue('installed', false) || (defined('PHPUNIT_RUN') && PHPUNIT_RUN)) { + return new \OC\Memcache\Factory( + $logger, $profiler, - /** @psalm-taint-escape callable */ - $config->getValue('memcache.local', null), - /** @psalm-taint-escape callable */ - $config->getValue('memcache.distributed', null), - /** @psalm-taint-escape callable */ - $config->getValue('memcache.locking', null), - /** @psalm-taint-escape callable */ - $config->getValue('redis_log_file') + $serverVersion, + ArrayCache::class, + ArrayCache::class, + ArrayCache::class ); } - return $arrayCacheFactory; + + return new \OC\Memcache\Factory( + $logger, + $profiler, + $serverVersion, + /** @psalm-taint-escape callable */ + $config->getValue('memcache.local', null), + /** @psalm-taint-escape callable */ + $config->getValue('memcache.distributed', null), + /** @psalm-taint-escape callable */ + $config->getValue('memcache.locking', null), + /** @psalm-taint-escape callable */ + $config->getValue('redis_log_file') + ); }); $this->registerAlias(ICacheFactory::class, Factory::class); diff --git a/lib/private/legacy/OC_App.php b/lib/private/legacy/OC_App.php index 10b78e2a7ef19..6dbef42594bf9 100644 --- a/lib/private/legacy/OC_App.php +++ b/lib/private/legacy/OC_App.php @@ -685,7 +685,7 @@ public static function updateApp(string $appId): bool { //set remote/public handlers if (array_key_exists('ocsid', $appData)) { \OC::$server->getConfig()->setAppValue($appId, 'ocsid', $appData['ocsid']); - } elseif (\OC::$server->getConfig()->getAppValue($appId, 'ocsid', null) !== null) { + } elseif (\OC::$server->getConfig()->getAppValue($appId, 'ocsid') !== '') { \OC::$server->getConfig()->deleteAppValue($appId, 'ocsid'); } foreach ($appData['remote'] as $name => $path) { diff --git a/lib/public/IConfig.php b/lib/public/IConfig.php index 3bc64c5e13337..d22606672aae4 100644 --- a/lib/public/IConfig.php +++ b/lib/public/IConfig.php @@ -112,8 +112,8 @@ public function getAppKeys($appName); * Writes a new app wide value * * @param string $appName the appName that we want to store the value under - * @param string|float|int $key the key of the value, under which will be saved - * @param string $value the value that should be stored + * @param string $key the key of the value, under which will be saved + * @param string|float|int $value the value that should be stored * @return void * @since 6.0.0 * @deprecated 29.0.0 Use {@see IAppConfig} directly diff --git a/tests/Core/Command/Encryption/EnableTest.php b/tests/Core/Command/Encryption/EnableTest.php index 32d1a7576f5f5..0e9655c29c778 100644 --- a/tests/Core/Command/Encryption/EnableTest.php +++ b/tests/Core/Command/Encryption/EnableTest.php @@ -48,9 +48,9 @@ protected function setUp(): void { public static function dataEnable(): array { return [ - ['no', null, [], true, 'Encryption enabled', 'No encryption module is loaded'], - ['yes', null, [], false, 'Encryption is already enabled', 'No encryption module is loaded'], - ['no', null, ['OC_TEST_MODULE' => []], true, 'Encryption enabled', 'No default module is set'], + ['no', '', [], true, 'Encryption enabled', 'No encryption module is loaded'], + ['yes', '', [], false, 'Encryption is already enabled', 'No encryption module is loaded'], + ['no', '', ['OC_TEST_MODULE' => []], true, 'Encryption enabled', 'No default module is set'], ['no', 'OC_NO_MODULE', ['OC_TEST_MODULE' => []], true, 'Encryption enabled', 'The current default module does not exist: OC_NO_MODULE'], ['no', 'OC_TEST_MODULE', ['OC_TEST_MODULE' => []], true, 'Encryption enabled', 'Default module: OC_TEST_MODULE'], ]; @@ -79,7 +79,7 @@ public function testEnable(string $oldStatus, ?string $defaultModule, array $ava ->method('getAppValue') ->willReturnMap([ ['core', 'encryption_enabled', 'no', $oldStatus], - ['core', 'default_encryption_module', null, $defaultModule], + ['core', 'default_encryption_module', '', $defaultModule], ]); } diff --git a/tests/lib/App/AppManagerTest.php b/tests/lib/App/AppManagerTest.php index 6637c529a1e53..9a4716aa8f10a 100644 --- a/tests/lib/App/AppManagerTest.php +++ b/tests/lib/App/AppManagerTest.php @@ -233,28 +233,25 @@ public function testEnableApp(): void { $this->manager->disableApp('files_trashbin'); } $this->eventDispatcher->expects($this->once())->method('dispatchTyped')->with(new AppEnableEvent('files_trashbin')); + $this->manager->enableApp('files_trashbin'); $this->assertEquals('yes', $this->appConfig->getValue('files_trashbin', 'enabled', 'no')); } public function testDisableApp(): void { $this->eventDispatcher->expects($this->once())->method('dispatchTyped')->with(new AppDisableEvent('files_trashbin')); + $this->manager->disableApp('files_trashbin'); $this->assertEquals('no', $this->appConfig->getValue('files_trashbin', 'enabled', 'no')); } public function testNotEnableIfNotInstalled(): void { - try { - $this->manager->enableApp('some_random_name_which_i_hope_is_not_an_app'); - $this->assertFalse(true, 'If this line is reached the expected exception is not thrown.'); - } catch (AppPathNotFoundException $e) { - // Exception is expected - $this->assertEquals('Could not find path for some_random_name_which_i_hope_is_not_an_app', $e->getMessage()); - } + $this->expectException(AppPathNotFoundException::class); + $this->expectExceptionMessage('Could not find path for some_random_name_which_i_hope_is_not_an_app'); + $this->appConfig->expects(self::never()) + ->method('setValue'); - $this->assertEquals('no', $this->appConfig->getValue( - 'some_random_name_which_i_hope_is_not_an_app', 'enabled', 'no' - )); + $this->manager->enableApp('some_random_name_which_i_hope_is_not_an_app'); } public function testEnableAppForGroups(): void { @@ -289,7 +286,9 @@ public function testEnableAppForGroups(): void { ->with('test') ->willReturn('apps/test'); - $this->eventDispatcher->expects($this->once())->method('dispatchTyped')->with(new AppEnableEvent('test', ['group1', 'group2'])); + $this->eventDispatcher->expects($this->once()) + ->method('dispatchTyped') + ->with(new AppEnableEvent('test', ['group1', 'group2'])); $manager->enableAppForGroups('test', $groups); $this->assertEquals('["group1","group2"]', $this->appConfig->getValue('test', 'enabled', 'no')); diff --git a/tests/lib/AppConfigIntegrationTest.php b/tests/lib/AppConfigIntegrationTest.php new file mode 100644 index 0000000000000..4f821e00a6354 --- /dev/null +++ b/tests/lib/AppConfigIntegrationTest.php @@ -0,0 +1,1515 @@ +>> + * [appId => [configKey, configValue, valueType, lazy, sensitive]] + */ + private static array $baseStruct + = [ + 'testapp' => [ + 'enabled' => ['enabled', 'yes'], + 'installed_version' => ['installed_version', '1.2.3'], + 'depends_on' => ['depends_on', 'someapp'], + 'deletethis' => ['deletethis', 'deletethis'], + 'key' => ['key', 'value'] + ], + 'searchtest' => [ + 'search_key1' => ['search_key1', 'key1', IAppConfig::VALUE_STRING], + 'search_key2' => ['search_key2', 'key2', IAppConfig::VALUE_STRING], + 'search_key3' => ['search_key3', 'key3', IAppConfig::VALUE_STRING], + 'searchnot_key4' => ['searchnot_key4', 'key4', IAppConfig::VALUE_STRING], + 'search_key5_lazy' => ['search_key5_lazy', 'key5', IAppConfig::VALUE_STRING, true], + ], + 'someapp' => [ + 'key' => ['key', 'value'], + 'otherkey' => ['otherkey', 'othervalue'] + ], + '123456' => [ + 'enabled' => ['enabled', 'yes'], + 'key' => ['key', 'value'] + ], + 'anotherapp' => [ + 'enabled' => ['enabled', 'no'], + 'installed_version' => ['installed_version', '3.2.1'], + 'key' => ['key', 'value'] + ], + 'non-sensitive-app' => [ + 'lazy-key' => ['lazy-key', 'value', IAppConfig::VALUE_STRING, true, false], + 'non-lazy-key' => ['non-lazy-key', 'value', IAppConfig::VALUE_STRING, false, false], + ], + 'sensitive-app' => [ + 'lazy-key' => ['lazy-key', 'value', IAppConfig::VALUE_STRING, true, true], + 'non-lazy-key' => ['non-lazy-key', 'value', IAppConfig::VALUE_STRING, false, true], + ], + 'only-lazy' => [ + 'lazy-key' => ['lazy-key', 'value', IAppConfig::VALUE_STRING, true] + ], + 'typed' => [ + 'mixed' => ['mixed', 'mix', IAppConfig::VALUE_MIXED], + 'string' => ['string', 'value', IAppConfig::VALUE_STRING], + 'int' => ['int', '42', IAppConfig::VALUE_INT], + 'float' => ['float', '3.14', IAppConfig::VALUE_FLOAT], + 'bool' => ['bool', '1', IAppConfig::VALUE_BOOL], + 'array' => ['array', '{"test": 1}', IAppConfig::VALUE_ARRAY], + ], + 'prefix-app' => [ + 'key1' => ['key1', 'value'], + 'prefix1' => ['prefix1', 'value'], + 'prefix-2' => ['prefix-2', 'value'], + 'key-2' => ['key-2', 'value'], + ] + ]; + + protected function setUp(): void { + parent::setUp(); + + $this->connection = Server::get(IDBConnection::class); + $this->config = Server::get(IConfig::class); + $this->configManager = Server::get(ConfigManager::class); + $this->presetManager = Server::get(PresetManager::class); + $this->logger = Server::get(LoggerInterface::class); + $this->crypto = Server::get(ICrypto::class); + $this->cacheFactory = $this->createMock(CacheFactory::class); + $this->cacheFactory->method('isLocalCacheAvailable')->willReturn(false); + + // storing current config and emptying the data table + $sql = $this->connection->getQueryBuilder(); + $sql->select('*') + ->from('appconfig'); + $result = $sql->executeQuery(); + $this->originalConfig = $result->fetchAll(); + $result->closeCursor(); + + $sql = $this->connection->getQueryBuilder(); + $sql->delete('appconfig'); + $sql->executeStatement(); + + $sql = $this->connection->getQueryBuilder(); + $sql->insert('appconfig') + ->values( + [ + 'appid' => $sql->createParameter('appid'), + 'configkey' => $sql->createParameter('configkey'), + 'configvalue' => $sql->createParameter('configvalue'), + 'type' => $sql->createParameter('type'), + 'lazy' => $sql->createParameter('lazy') + ] + ); + + foreach (self::$baseStruct as $appId => $appData) { + foreach ($appData as $key => $row) { + $value = $row[1]; + $type = $row[2] ?? IAppConfig::VALUE_MIXED; + if (($row[4] ?? false) === true) { + $type |= IAppConfig::VALUE_SENSITIVE; + $value = self::invokePrivate(AppConfig::class, 'ENCRYPTION_PREFIX') . $this->crypto->encrypt($value); + self::$baseStruct[$appId][$key]['encrypted'] = $value; + } + + $sql->setParameters( + [ + 'appid' => $appId, + 'configkey' => $row[0], + 'configvalue' => $value, + 'type' => $type, + 'lazy' => (($row[3] ?? false) === true) ? 1 : 0 + ] + )->executeStatement(); + } + } + } + + protected function tearDown(): void { + $sql = $this->connection->getQueryBuilder(); + $sql->delete('appconfig'); + $sql->executeStatement(); + + $sql = $this->connection->getQueryBuilder(); + $sql->insert('appconfig') + ->values( + [ + 'appid' => $sql->createParameter('appid'), + 'configkey' => $sql->createParameter('configkey'), + 'configvalue' => $sql->createParameter('configvalue'), + 'lazy' => $sql->createParameter('lazy'), + 'type' => $sql->createParameter('type'), + ] + ); + + foreach ($this->originalConfig as $key => $configs) { + $sql->setParameter('appid', $configs['appid']) + ->setParameter('configkey', $configs['configkey']) + ->setParameter('configvalue', $configs['configvalue']) + ->setParameter('lazy', ($configs['lazy'] === '1') ? '1' : '0') + ->setParameter('type', $configs['type']); + $sql->executeStatement(); + } + + // $this->restoreService(AppConfig::class); + parent::tearDown(); + } + + /** + * @param bool $preLoading TRUE will preload the 'fast' cache, which is the normal behavior of usual + * IAppConfig + * + * @return IAppConfig + */ + private function generateAppConfig(bool $preLoading = true): IAppConfig { + /** @var AppConfig $config */ + $config = new AppConfig( + $this->connection, + $this->config, + $this->configManager, + $this->presetManager, + $this->logger, + $this->crypto, + $this->cacheFactory, + ); + $msg = ' generateAppConfig() failed to confirm cache status'; + + // confirm cache status + $status = $config->statusCache(); + $this->assertSame(false, $status['fastLoaded'], $msg); + $this->assertSame(false, $status['lazyLoaded'], $msg); + $this->assertSame([], $status['fastCache'], $msg); + $this->assertSame([], $status['lazyCache'], $msg); + if ($preLoading) { + // simple way to initiate the load of non-lazy config values in cache + $config->getValueString('core', 'preload', ''); + + // confirm cache status + $status = $config->statusCache(); + $this->assertSame(true, $status['fastLoaded'], $msg); + $this->assertSame(false, $status['lazyLoaded'], $msg); + + $apps = array_values(array_diff(array_keys(self::$baseStruct), ['only-lazy'])); + $this->assertEqualsCanonicalizing($apps, array_keys($status['fastCache']), $msg); + $this->assertSame([], array_keys($status['lazyCache']), $msg); + } + + return $config; + } + + public function testGetApps(): void { + $config = $this->generateAppConfig(false); + + $this->assertEqualsCanonicalizing(array_keys(self::$baseStruct), $config->getApps()); + } + + public function testGetAppInstalledVersions(): void { + $config = $this->generateAppConfig(false); + + $this->assertEquals( + ['testapp' => '1.2.3', 'anotherapp' => '3.2.1'], + $config->getAppInstalledVersions(false) + ); + $this->assertEquals( + ['testapp' => '1.2.3'], + $config->getAppInstalledVersions(true) + ); + } + + /** + * returns list of app and their keys + * + * @return array ['appId' => ['key1', 'key2', ]] + * @see testGetKeys + */ + public static function providerGetAppKeys(): array { + $appKeys = []; + foreach (self::$baseStruct as $appId => $appData) { + $keys = []; + foreach ($appData as $row) { + $keys[] = $row[0]; + } + $appKeys[] = [(string)$appId, $keys]; + } + + return $appKeys; + } + + /** + * returns list of config keys + * + * @return array [appId, key, value, type, lazy, sensitive] + * @see testIsSensitive + * @see testIsLazy + * @see testGetKeys + */ + public static function providerGetKeys(): array { + $appKeys = []; + foreach (self::$baseStruct as $appId => $appData) { + foreach ($appData as $row) { + $appKeys[] = [ + (string)$appId, $row[0], $row[1], $row[2] ?? IAppConfig::VALUE_MIXED, $row[3] ?? false, + $row[4] ?? false + ]; + } + } + + return $appKeys; + } + + /** + * + * @param string $appId + * @param array $expectedKeys + */ + #[\PHPUnit\Framework\Attributes\DataProvider('providerGetAppKeys')] + public function testGetKeys(string $appId, array $expectedKeys): void { + $config = $this->generateAppConfig(); + $this->assertEqualsCanonicalizing($expectedKeys, $config->getKeys($appId)); + } + + public function testGetKeysOnUnknownAppShouldReturnsEmptyArray(): void { + $config = $this->generateAppConfig(); + $this->assertEqualsCanonicalizing([], $config->getKeys('unknown-app')); + } + + /** + * + * @param string $appId + * @param string $configKey + * @param string $value + * @param bool $lazy + */ + #[\PHPUnit\Framework\Attributes\DataProvider('providerGetKeys')] + public function testHasKey(string $appId, string $configKey, string $value, int $type, bool $lazy): void { + $config = $this->generateAppConfig(); + $this->assertEquals(true, $config->hasKey($appId, $configKey, $lazy)); + } + + public function testHasKeyOnNonExistentKeyReturnsFalse(): void { + $config = $this->generateAppConfig(); + $this->assertEquals(false, $config->hasKey(array_keys(self::$baseStruct)[0], 'inexistant-key')); + } + + public function testHasKeyOnUnknownAppReturnsFalse(): void { + $config = $this->generateAppConfig(); + $this->assertEquals(false, $config->hasKey('inexistant-app', 'inexistant-key')); + } + + public function testHasKeyOnMistypedAsLazyReturnsFalse(): void { + $config = $this->generateAppConfig(); + $this->assertSame(false, $config->hasKey('non-sensitive-app', 'non-lazy-key', true)); + } + + public function testHasKeyOnMistypeAsNonLazyReturnsFalse(): void { + $config = $this->generateAppConfig(); + $this->assertSame(false, $config->hasKey('non-sensitive-app', 'lazy-key', false)); + } + + public function testHasKeyOnMistypeAsNonLazyReturnsTrueWithLazyArgumentIsNull(): void { + $config = $this->generateAppConfig(); + $this->assertSame(true, $config->hasKey('non-sensitive-app', 'lazy-key', null)); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('providerGetKeys')] + public function testIsSensitive( + string $appId, string $configKey, string $configValue, int $type, bool $lazy, bool $sensitive, + ): void { + $config = $this->generateAppConfig(); + $this->assertEquals($sensitive, $config->isSensitive($appId, $configKey, $lazy)); + } + + public function testIsSensitiveOnNonExistentKeyThrowsException(): void { + $config = $this->generateAppConfig(); + $this->expectException(AppConfigUnknownKeyException::class); + $config->isSensitive(array_keys(self::$baseStruct)[0], 'inexistant-key'); + } + + public function testIsSensitiveOnUnknownAppThrowsException(): void { + $config = $this->generateAppConfig(); + $this->expectException(AppConfigUnknownKeyException::class); + $config->isSensitive('unknown-app', 'inexistant-key'); + } + + public function testIsSensitiveOnSensitiveMistypedAsLazy(): void { + $config = $this->generateAppConfig(); + $this->assertSame(true, $config->isSensitive('sensitive-app', 'non-lazy-key', true)); + } + + public function testIsSensitiveOnNonSensitiveMistypedAsLazy(): void { + $config = $this->generateAppConfig(); + $this->assertSame(false, $config->isSensitive('non-sensitive-app', 'non-lazy-key', true)); + } + + public function testIsSensitiveOnSensitiveMistypedAsNonLazyThrowsException(): void { + $config = $this->generateAppConfig(); + $this->expectException(AppConfigUnknownKeyException::class); + $config->isSensitive('sensitive-app', 'lazy-key', false); + } + + public function testIsSensitiveOnNonSensitiveMistypedAsNonLazyThrowsException(): void { + $config = $this->generateAppConfig(); + $this->expectException(AppConfigUnknownKeyException::class); + $config->isSensitive('non-sensitive-app', 'lazy-key', false); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('providerGetKeys')] + public function testIsLazy(string $appId, string $configKey, string $configValue, int $type, bool $lazy, + ): void { + $config = $this->generateAppConfig(); + $this->assertEquals($lazy, $config->isLazy($appId, $configKey)); + } + + public function testIsLazyOnNonExistentKeyThrowsException(): void { + $config = $this->generateAppConfig(); + $this->expectException(AppConfigUnknownKeyException::class); + $config->isLazy(array_keys(self::$baseStruct)[0], 'inexistant-key'); + } + + public function testIsLazyOnUnknownAppThrowsException(): void { + $config = $this->generateAppConfig(); + $this->expectException(AppConfigUnknownKeyException::class); + $config->isLazy('unknown-app', 'inexistant-key'); + } + + public function testGetAllValues(): void { + $config = $this->generateAppConfig(); + $this->assertEquals( + [ + 'array' => ['test' => 1], + 'bool' => true, + 'float' => 3.14, + 'int' => 42, + 'mixed' => 'mix', + 'string' => 'value', + ], + $config->getAllValues('typed') + ); + } + + public function testGetAllValuesWithEmptyApp(): void { + $config = $this->generateAppConfig(); + $this->expectException(InvalidArgumentException::class); + $config->getAllValues(''); + } + + /** + * + * @param string $appId + * @param array $keys + */ + #[\PHPUnit\Framework\Attributes\DataProvider('providerGetAppKeys')] + public function testGetAllValuesWithEmptyKey(string $appId, array $keys): void { + $config = $this->generateAppConfig(); + $this->assertEqualsCanonicalizing($keys, array_keys($config->getAllValues($appId, ''))); + } + + public function testGetAllValuesWithPrefix(): void { + $config = $this->generateAppConfig(); + $this->assertEqualsCanonicalizing(['prefix1', 'prefix-2'], array_keys($config->getAllValues('prefix-app', 'prefix'))); + } + + public function testSearchValues(): void { + $config = $this->generateAppConfig(); + $this->assertEqualsCanonicalizing(['testapp' => 'yes', '123456' => 'yes', 'anotherapp' => 'no'], $config->searchValues('enabled')); + } + + public function testGetValueString(): void { + $config = $this->generateAppConfig(); + $this->assertSame('value', $config->getValueString('typed', 'string', '')); + } + + public function testGetValueStringOnUnknownAppReturnsDefault(): void { + $config = $this->generateAppConfig(); + $this->assertSame('default-1', $config->getValueString('typed-1', 'string', 'default-1')); + } + + public function testGetValueStringOnNonExistentKeyReturnsDefault(): void { + $config = $this->generateAppConfig(); + $this->assertSame('default-2', $config->getValueString('typed', 'string-2', 'default-2')); + } + + public function testGetValueStringOnWrongType(): void { + $config = $this->generateAppConfig(); + $this->expectException(AppConfigTypeConflictException::class); + $config->getValueString('typed', 'int'); + } + + public function testGetNonLazyValueStringAsLazy(): void { + $config = $this->generateAppConfig(); + $this->assertSame('value', $config->getValueString('non-sensitive-app', 'non-lazy-key', 'default', lazy: true)); + } + + public function testGetValueInt(): void { + $config = $this->generateAppConfig(); + $this->assertSame(42, $config->getValueInt('typed', 'int', 0)); + } + + public function testGetValueIntOnUnknownAppReturnsDefault(): void { + $config = $this->generateAppConfig(); + $this->assertSame(1, $config->getValueInt('typed-1', 'int', 1)); + } + + public function testGetValueIntOnNonExistentKeyReturnsDefault(): void { + $config = $this->generateAppConfig(); + $this->assertSame(2, $config->getValueInt('typed', 'int-2', 2)); + } + + public function testGetValueIntOnWrongType(): void { + $config = $this->generateAppConfig(); + $this->expectException(AppConfigTypeConflictException::class); + $config->getValueInt('typed', 'float'); + } + + public function testGetValueFloat(): void { + $config = $this->generateAppConfig(); + $this->assertSame(3.14, $config->getValueFloat('typed', 'float', 0)); + } + + public function testGetValueFloatOnNonUnknownAppReturnsDefault(): void { + $config = $this->generateAppConfig(); + $this->assertSame(1.11, $config->getValueFloat('typed-1', 'float', 1.11)); + } + + public function testGetValueFloatOnNonExistentKeyReturnsDefault(): void { + $config = $this->generateAppConfig(); + $this->assertSame(2.22, $config->getValueFloat('typed', 'float-2', 2.22)); + } + + public function testGetValueFloatOnWrongType(): void { + $config = $this->generateAppConfig(); + $this->expectException(AppConfigTypeConflictException::class); + $config->getValueFloat('typed', 'bool'); + } + + public function testGetValueBool(): void { + $config = $this->generateAppConfig(); + $this->assertSame(true, $config->getValueBool('typed', 'bool')); + } + + public function testGetValueBoolOnUnknownAppReturnsDefault(): void { + $config = $this->generateAppConfig(); + $this->assertSame(false, $config->getValueBool('typed-1', 'bool', false)); + } + + public function testGetValueBoolOnNonExistentKeyReturnsDefault(): void { + $config = $this->generateAppConfig(); + $this->assertSame(false, $config->getValueBool('typed', 'bool-2')); + } + + public function testGetValueBoolOnWrongType(): void { + $config = $this->generateAppConfig(); + $this->expectException(AppConfigTypeConflictException::class); + $config->getValueBool('typed', 'array'); + } + + public function testGetValueArray(): void { + $config = $this->generateAppConfig(); + $this->assertEqualsCanonicalizing(['test' => 1], $config->getValueArray('typed', 'array', [])); + } + + public function testGetValueArrayOnUnknownAppReturnsDefault(): void { + $config = $this->generateAppConfig(); + $this->assertSame([1], $config->getValueArray('typed-1', 'array', [1])); + } + + public function testGetValueArrayOnNonExistentKeyReturnsDefault(): void { + $config = $this->generateAppConfig(); + $this->assertSame([1, 2], $config->getValueArray('typed', 'array-2', [1, 2])); + } + + public function testGetValueArrayOnWrongType(): void { + $config = $this->generateAppConfig(); + $this->expectException(AppConfigTypeConflictException::class); + $config->getValueArray('typed', 'string'); + } + + + /** + * @return array + * @see testGetValueType + * + * @see testGetValueMixed + */ + public static function providerGetValueMixed(): array { + return [ + // key, value, type + ['mixed', 'mix', IAppConfig::VALUE_MIXED], + ['string', 'value', IAppConfig::VALUE_STRING], + ['int', '42', IAppConfig::VALUE_INT], + ['float', '3.14', IAppConfig::VALUE_FLOAT], + ['bool', '1', IAppConfig::VALUE_BOOL], + ['array', '{"test": 1}', IAppConfig::VALUE_ARRAY], + ]; + } + + /** + * + * @param string $key + * @param string $value + */ + #[\PHPUnit\Framework\Attributes\DataProvider('providerGetValueMixed')] + public function testGetValueMixed(string $key, string $value): void { + $config = $this->generateAppConfig(); + $this->assertSame($value, $config->getValueMixed('typed', $key)); + } + + /** + * + * @param string $key + * @param string $value + * @param int $type + */ + #[\PHPUnit\Framework\Attributes\DataProvider('providerGetValueMixed')] + public function testGetValueType(string $key, string $value, int $type): void { + $config = $this->generateAppConfig(); + $this->assertSame($type, $config->getValueType('typed', $key)); + } + + public function testGetValueTypeOnUnknownApp(): void { + $config = $this->generateAppConfig(); + $this->expectException(AppConfigUnknownKeyException::class); + $config->getValueType('typed-1', 'string'); + } + + public function testGetValueTypeOnNonExistentKey(): void { + $config = $this->generateAppConfig(); + $this->expectException(AppConfigUnknownKeyException::class); + $config->getValueType('typed', 'string-2'); + } + + public function testSetValueString(): void { + $config = $this->generateAppConfig(); + $config->setValueString('feed', 'string', 'value-1'); + $this->assertSame('value-1', $config->getValueString('feed', 'string', '')); + } + + public function testSetValueStringCache(): void { + $config = $this->generateAppConfig(); + $config->setValueString('feed', 'string', 'value-1'); + $status = $config->statusCache(); + $this->assertSame('value-1', $status['fastCache']['feed']['string']); + } + + public function testSetValueStringDatabase(): void { + $config = $this->generateAppConfig(); + $config->setValueString('feed', 'string', 'value-1'); + $config->clearCache(); + $this->assertSame('value-1', $config->getValueString('feed', 'string', '')); + } + + public function testSetValueStringIsUpdated(): void { + $config = $this->generateAppConfig(); + $config->setValueString('feed', 'string', 'value-1'); + $this->assertSame(true, $config->setValueString('feed', 'string', 'value-2')); + } + + public function testSetValueStringIsNotUpdated(): void { + $config = $this->generateAppConfig(); + $config->setValueString('feed', 'string', 'value-1'); + $this->assertSame(false, $config->setValueString('feed', 'string', 'value-1')); + } + + public function testSetValueStringIsUpdatedCache(): void { + $config = $this->generateAppConfig(); + $config->setValueString('feed', 'string', 'value-1'); + $config->setValueString('feed', 'string', 'value-2'); + $status = $config->statusCache(); + $this->assertSame('value-2', $status['fastCache']['feed']['string']); + } + + public function testSetValueStringIsUpdatedDatabase(): void { + $config = $this->generateAppConfig(); + $config->setValueString('feed', 'string', 'value-1'); + $config->setValueString('feed', 'string', 'value-2'); + $config->clearCache(); + $this->assertSame('value-2', $config->getValueString('feed', 'string', '')); + } + + public function testSetValueInt(): void { + $config = $this->generateAppConfig(); + $config->setValueInt('feed', 'int', 42); + $this->assertSame(42, $config->getValueInt('feed', 'int', 0)); + } + + public function testSetValueIntCache(): void { + $config = $this->generateAppConfig(); + $config->setValueInt('feed', 'int', 42); + $status = $config->statusCache(); + $this->assertSame('42', $status['fastCache']['feed']['int']); + } + + public function testSetValueIntDatabase(): void { + $config = $this->generateAppConfig(); + $config->setValueInt('feed', 'int', 42); + $config->clearCache(); + $this->assertSame(42, $config->getValueInt('feed', 'int', 0)); + } + + public function testSetValueIntIsUpdated(): void { + $config = $this->generateAppConfig(); + $config->setValueInt('feed', 'int', 42); + $this->assertSame(true, $config->setValueInt('feed', 'int', 17)); + } + + public function testSetValueIntIsNotUpdated(): void { + $config = $this->generateAppConfig(); + $config->setValueInt('feed', 'int', 42); + $this->assertSame(false, $config->setValueInt('feed', 'int', 42)); + } + + public function testSetValueIntIsUpdatedCache(): void { + $config = $this->generateAppConfig(); + $config->setValueInt('feed', 'int', 42); + $config->setValueInt('feed', 'int', 17); + $status = $config->statusCache(); + $this->assertSame('17', $status['fastCache']['feed']['int']); + } + + public function testSetValueIntIsUpdatedDatabase(): void { + $config = $this->generateAppConfig(); + $config->setValueInt('feed', 'int', 42); + $config->setValueInt('feed', 'int', 17); + $config->clearCache(); + $this->assertSame(17, $config->getValueInt('feed', 'int', 0)); + } + + public function testSetValueFloat(): void { + $config = $this->generateAppConfig(); + $config->setValueFloat('feed', 'float', 3.14); + $this->assertSame(3.14, $config->getValueFloat('feed', 'float', 0)); + } + + public function testSetValueFloatCache(): void { + $config = $this->generateAppConfig(); + $config->setValueFloat('feed', 'float', 3.14); + $status = $config->statusCache(); + $this->assertSame('3.14', $status['fastCache']['feed']['float']); + } + + public function testSetValueFloatDatabase(): void { + $config = $this->generateAppConfig(); + $config->setValueFloat('feed', 'float', 3.14); + $config->clearCache(); + $this->assertSame(3.14, $config->getValueFloat('feed', 'float', 0)); + } + + public function testSetValueFloatIsUpdated(): void { + $config = $this->generateAppConfig(); + $config->setValueFloat('feed', 'float', 3.14); + $this->assertSame(true, $config->setValueFloat('feed', 'float', 1.23)); + } + + public function testSetValueFloatIsNotUpdated(): void { + $config = $this->generateAppConfig(); + $config->setValueFloat('feed', 'float', 3.14); + $this->assertSame(false, $config->setValueFloat('feed', 'float', 3.14)); + } + + public function testSetValueFloatIsUpdatedCache(): void { + $config = $this->generateAppConfig(); + $config->setValueFloat('feed', 'float', 3.14); + $config->setValueFloat('feed', 'float', 1.23); + $status = $config->statusCache(); + $this->assertSame('1.23', $status['fastCache']['feed']['float']); + } + + public function testSetValueFloatIsUpdatedDatabase(): void { + $config = $this->generateAppConfig(); + $config->setValueFloat('feed', 'float', 3.14); + $config->setValueFloat('feed', 'float', 1.23); + $config->clearCache(); + $this->assertSame(1.23, $config->getValueFloat('feed', 'float', 0)); + } + + public function testSetValueBool(): void { + $config = $this->generateAppConfig(); + $config->setValueBool('feed', 'bool', true); + $this->assertSame(true, $config->getValueBool('feed', 'bool', false)); + } + + public function testSetValueBoolCache(): void { + $config = $this->generateAppConfig(); + $config->setValueBool('feed', 'bool', true); + $status = $config->statusCache(); + $this->assertSame('1', $status['fastCache']['feed']['bool']); + } + + public function testSetValueBoolDatabase(): void { + $config = $this->generateAppConfig(); + $config->setValueBool('feed', 'bool', true); + $config->clearCache(); + $this->assertSame(true, $config->getValueBool('feed', 'bool', false)); + } + + public function testSetValueBoolIsUpdated(): void { + $config = $this->generateAppConfig(); + $config->setValueBool('feed', 'bool', true); + $this->assertSame(true, $config->setValueBool('feed', 'bool', false)); + } + + public function testSetValueBoolIsNotUpdated(): void { + $config = $this->generateAppConfig(); + $config->setValueBool('feed', 'bool', true); + $this->assertSame(false, $config->setValueBool('feed', 'bool', true)); + } + + public function testSetValueBoolIsUpdatedCache(): void { + $config = $this->generateAppConfig(); + $config->setValueBool('feed', 'bool', true); + $config->setValueBool('feed', 'bool', false); + $status = $config->statusCache(); + $this->assertSame('0', $status['fastCache']['feed']['bool']); + } + + public function testSetValueBoolIsUpdatedDatabase(): void { + $config = $this->generateAppConfig(); + $config->setValueBool('feed', 'bool', true); + $config->setValueBool('feed', 'bool', false); + $config->clearCache(); + $this->assertSame(false, $config->getValueBool('feed', 'bool', true)); + } + + + public function testSetValueArray(): void { + $config = $this->generateAppConfig(); + $config->setValueArray('feed', 'array', ['test' => 1]); + $this->assertSame(['test' => 1], $config->getValueArray('feed', 'array', [])); + } + + public function testSetValueArrayCache(): void { + $config = $this->generateAppConfig(); + $config->setValueArray('feed', 'array', ['test' => 1]); + $status = $config->statusCache(); + $this->assertSame('{"test":1}', $status['fastCache']['feed']['array']); + } + + public function testSetValueArrayDatabase(): void { + $config = $this->generateAppConfig(); + $config->setValueArray('feed', 'array', ['test' => 1]); + $config->clearCache(); + $this->assertSame(['test' => 1], $config->getValueArray('feed', 'array', [])); + } + + public function testSetValueArrayIsUpdated(): void { + $config = $this->generateAppConfig(); + $config->setValueArray('feed', 'array', ['test' => 1]); + $this->assertSame(true, $config->setValueArray('feed', 'array', ['test' => 2])); + } + + public function testSetValueArrayIsNotUpdated(): void { + $config = $this->generateAppConfig(); + $config->setValueArray('feed', 'array', ['test' => 1]); + $this->assertSame(false, $config->setValueArray('feed', 'array', ['test' => 1])); + } + + public function testSetValueArrayIsUpdatedCache(): void { + $config = $this->generateAppConfig(); + $config->setValueArray('feed', 'array', ['test' => 1]); + $config->setValueArray('feed', 'array', ['test' => 2]); + $status = $config->statusCache(); + $this->assertSame('{"test":2}', $status['fastCache']['feed']['array']); + } + + public function testSetValueArrayIsUpdatedDatabase(): void { + $config = $this->generateAppConfig(); + $config->setValueArray('feed', 'array', ['test' => 1]); + $config->setValueArray('feed', 'array', ['test' => 2]); + $config->clearCache(); + $this->assertSame(['test' => 2], $config->getValueArray('feed', 'array', [])); + } + + public function testSetLazyValueString(): void { + $config = $this->generateAppConfig(); + $config->setValueString('feed', 'string', 'value-1', true); + $this->assertSame('value-1', $config->getValueString('feed', 'string', '', true)); + } + + public function testSetLazyValueStringCache(): void { + $config = $this->generateAppConfig(); + $config->setValueString('feed', 'string', 'value-1', true); + $status = $config->statusCache(); + $this->assertSame('value-1', $status['lazyCache']['feed']['string']); + } + + public function testSetLazyValueStringDatabase(): void { + $config = $this->generateAppConfig(); + $config->setValueString('feed', 'string', 'value-1', true); + $config->clearCache(); + $this->assertSame('value-1', $config->getValueString('feed', 'string', '', true)); + } + + public function testSetLazyValueStringAsNonLazy(): void { + $config = $this->generateAppConfig(); + $config->setValueString('feed', 'string', 'value-1', true); + $config->setValueString('feed', 'string', 'value-1', false); + $this->assertSame('value-1', $config->getValueString('feed', 'string', '')); + } + + public function testSetNonLazyValueStringAsLazy(): void { + $config = $this->generateAppConfig(); + $config->setValueString('feed', 'string', 'value-1', false); + $config->setValueString('feed', 'string', 'value-1', true); + $this->assertSame('value-1', $config->getValueString('feed', 'string', '', true)); + } + + public function testSetSensitiveValueString(): void { + $config = $this->generateAppConfig(); + $config->setValueString('feed', 'string', 'value-1', sensitive: true); + $this->assertSame('value-1', $config->getValueString('feed', 'string', '')); + } + + public function testSetSensitiveValueStringCache(): void { + $config = $this->generateAppConfig(); + $config->setValueString('feed', 'string', 'value-1', sensitive: true); + $status = $config->statusCache(); + $this->assertStringStartsWith(self::invokePrivate(AppConfig::class, 'ENCRYPTION_PREFIX'), $status['fastCache']['feed']['string']); + } + + public function testSetSensitiveValueStringDatabase(): void { + $config = $this->generateAppConfig(); + $config->setValueString('feed', 'string', 'value-1', sensitive: true); + $config->clearCache(); + $this->assertSame('value-1', $config->getValueString('feed', 'string', '')); + } + + public function testSetNonSensitiveValueStringAsSensitive(): void { + $config = $this->generateAppConfig(); + $config->setValueString('feed', 'string', 'value-1', sensitive: false); + $config->setValueString('feed', 'string', 'value-1', sensitive: true); + $this->assertSame(true, $config->isSensitive('feed', 'string')); + + $this->assertConfigValueNotEquals('feed', 'string', 'value-1'); + $this->assertConfigValueNotEquals('feed', 'string', 'value-2'); + } + + public function testSetSensitiveValueStringAsNonSensitiveStaysSensitive(): void { + $config = $this->generateAppConfig(); + $config->setValueString('feed', 'string', 'value-1', sensitive: true); + $config->setValueString('feed', 'string', 'value-2', sensitive: false); + $this->assertSame(true, $config->isSensitive('feed', 'string')); + + $this->assertConfigValueNotEquals('feed', 'string', 'value-1'); + $this->assertConfigValueNotEquals('feed', 'string', 'value-2'); + } + + public function testSetSensitiveValueStringAsNonSensitiveAreStillUpdated(): void { + $config = $this->generateAppConfig(); + $config->setValueString('feed', 'string', 'value-1', sensitive: true); + $config->setValueString('feed', 'string', 'value-2', sensitive: false); + $this->assertSame('value-2', $config->getValueString('feed', 'string', '')); + + $this->assertConfigValueNotEquals('feed', 'string', 'value-1'); + $this->assertConfigValueNotEquals('feed', 'string', 'value-2'); + } + + public function testSetLazyValueInt(): void { + $config = $this->generateAppConfig(); + $config->setValueInt('feed', 'int', 42, true); + $this->assertSame(42, $config->getValueInt('feed', 'int', 0, true)); + } + + public function testSetLazyValueIntCache(): void { + $config = $this->generateAppConfig(); + $config->setValueInt('feed', 'int', 42, true); + $status = $config->statusCache(); + $this->assertSame('42', $status['lazyCache']['feed']['int']); + } + + public function testSetLazyValueIntDatabase(): void { + $config = $this->generateAppConfig(); + $config->setValueInt('feed', 'int', 42, true); + $config->clearCache(); + $this->assertSame(42, $config->getValueInt('feed', 'int', 0, true)); + } + + public function testSetLazyValueIntAsNonLazy(): void { + $config = $this->generateAppConfig(); + $config->setValueInt('feed', 'int', 42, true); + $config->setValueInt('feed', 'int', 42, false); + $this->assertSame(42, $config->getValueInt('feed', 'int', 0)); + } + + public function testSetNonLazyValueIntAsLazy(): void { + $config = $this->generateAppConfig(); + $config->setValueInt('feed', 'int', 42, false); + $config->setValueInt('feed', 'int', 42, true); + $this->assertSame(42, $config->getValueInt('feed', 'int', 0, true)); + } + + public function testSetSensitiveValueInt(): void { + $config = $this->generateAppConfig(); + $config->setValueInt('feed', 'int', 42, sensitive: true); + $this->assertSame(42, $config->getValueInt('feed', 'int', 0)); + } + + public function testSetSensitiveValueIntCache(): void { + $config = $this->generateAppConfig(); + $config->setValueInt('feed', 'int', 42, sensitive: true); + $status = $config->statusCache(); + $this->assertStringStartsWith(self::invokePrivate(AppConfig::class, 'ENCRYPTION_PREFIX'), $status['fastCache']['feed']['int']); + } + + public function testSetSensitiveValueIntDatabase(): void { + $config = $this->generateAppConfig(); + $config->setValueInt('feed', 'int', 42, sensitive: true); + $config->clearCache(); + $this->assertSame(42, $config->getValueInt('feed', 'int', 0)); + } + + public function testSetNonSensitiveValueIntAsSensitive(): void { + $config = $this->generateAppConfig(); + $config->setValueInt('feed', 'int', 42); + $config->setValueInt('feed', 'int', 42, sensitive: true); + $this->assertSame(true, $config->isSensitive('feed', 'int')); + } + + public function testSetSensitiveValueIntAsNonSensitiveStaysSensitive(): void { + $config = $this->generateAppConfig(); + $config->setValueInt('feed', 'int', 42, sensitive: true); + $config->setValueInt('feed', 'int', 17); + $this->assertSame(true, $config->isSensitive('feed', 'int')); + } + + public function testSetSensitiveValueIntAsNonSensitiveAreStillUpdated(): void { + $config = $this->generateAppConfig(); + $config->setValueInt('feed', 'int', 42, sensitive: true); + $config->setValueInt('feed', 'int', 17); + $this->assertSame(17, $config->getValueInt('feed', 'int', 0)); + } + + public function testSetLazyValueFloat(): void { + $config = $this->generateAppConfig(); + $config->setValueFloat('feed', 'float', 3.14, true); + $this->assertSame(3.14, $config->getValueFloat('feed', 'float', 0, true)); + } + + public function testSetLazyValueFloatCache(): void { + $config = $this->generateAppConfig(); + $config->setValueFloat('feed', 'float', 3.14, true); + $status = $config->statusCache(); + $this->assertSame('3.14', $status['lazyCache']['feed']['float']); + } + + public function testSetLazyValueFloatDatabase(): void { + $config = $this->generateAppConfig(); + $config->setValueFloat('feed', 'float', 3.14, true); + $config->clearCache(); + $this->assertSame(3.14, $config->getValueFloat('feed', 'float', 0, true)); + } + + public function testSetLazyValueFloatAsNonLazy(): void { + $config = $this->generateAppConfig(); + $config->setValueFloat('feed', 'float', 3.14, true); + $config->setValueFloat('feed', 'float', 3.14, false); + $this->assertSame(3.14, $config->getValueFloat('feed', 'float', 0)); + } + + public function testSetNonLazyValueFloatAsLazy(): void { + $config = $this->generateAppConfig(); + $config->setValueFloat('feed', 'float', 3.14, false); + $config->setValueFloat('feed', 'float', 3.14, true); + $this->assertSame(3.14, $config->getValueFloat('feed', 'float', 0, true)); + } + + public function testSetSensitiveValueFloat(): void { + $config = $this->generateAppConfig(); + $config->setValueFloat('feed', 'float', 3.14, sensitive: true); + $this->assertSame(3.14, $config->getValueFloat('feed', 'float', 0)); + } + + public function testSetSensitiveValueFloatCache(): void { + $config = $this->generateAppConfig(); + $config->setValueFloat('feed', 'float', 3.14, sensitive: true); + $status = $config->statusCache(); + $this->assertStringStartsWith(self::invokePrivate(AppConfig::class, 'ENCRYPTION_PREFIX'), $status['fastCache']['feed']['float']); + } + + public function testSetSensitiveValueFloatDatabase(): void { + $config = $this->generateAppConfig(); + $config->setValueFloat('feed', 'float', 3.14, sensitive: true); + $config->clearCache(); + $this->assertSame(3.14, $config->getValueFloat('feed', 'float', 0)); + } + + public function testSetNonSensitiveValueFloatAsSensitive(): void { + $config = $this->generateAppConfig(); + $config->setValueFloat('feed', 'float', 3.14); + $config->setValueFloat('feed', 'float', 3.14, sensitive: true); + $this->assertSame(true, $config->isSensitive('feed', 'float')); + } + + public function testSetSensitiveValueFloatAsNonSensitiveStaysSensitive(): void { + $config = $this->generateAppConfig(); + $config->setValueFloat('feed', 'float', 3.14, sensitive: true); + $config->setValueFloat('feed', 'float', 1.23); + $this->assertSame(true, $config->isSensitive('feed', 'float')); + } + + public function testSetSensitiveValueFloatAsNonSensitiveAreStillUpdated(): void { + $config = $this->generateAppConfig(); + $config->setValueFloat('feed', 'float', 3.14, sensitive: true); + $config->setValueFloat('feed', 'float', 1.23); + $this->assertSame(1.23, $config->getValueFloat('feed', 'float', 0)); + } + + public function testSetLazyValueBool(): void { + $config = $this->generateAppConfig(); + $config->setValueBool('feed', 'bool', true, true); + $this->assertSame(true, $config->getValueBool('feed', 'bool', false, true)); + } + + public function testSetLazyValueBoolCache(): void { + $config = $this->generateAppConfig(); + $config->setValueBool('feed', 'bool', true, true); + $status = $config->statusCache(); + $this->assertSame('1', $status['lazyCache']['feed']['bool']); + } + + public function testSetLazyValueBoolDatabase(): void { + $config = $this->generateAppConfig(); + $config->setValueBool('feed', 'bool', true, true); + $config->clearCache(); + $this->assertSame(true, $config->getValueBool('feed', 'bool', false, true)); + } + + public function testSetLazyValueBoolAsNonLazy(): void { + $config = $this->generateAppConfig(); + $config->setValueBool('feed', 'bool', true, true); + $config->setValueBool('feed', 'bool', true, false); + $this->assertSame(true, $config->getValueBool('feed', 'bool', false)); + } + + public function testSetNonLazyValueBoolAsLazy(): void { + $config = $this->generateAppConfig(); + $config->setValueBool('feed', 'bool', true, false); + $config->setValueBool('feed', 'bool', true, true); + $this->assertSame(true, $config->getValueBool('feed', 'bool', false, true)); + } + + public function testSetLazyValueArray(): void { + $config = $this->generateAppConfig(); + $config->setValueArray('feed', 'array', ['test' => 1], true); + $this->assertSame(['test' => 1], $config->getValueArray('feed', 'array', [], true)); + } + + public function testSetLazyValueArrayCache(): void { + $config = $this->generateAppConfig(); + $config->setValueArray('feed', 'array', ['test' => 1], true); + $status = $config->statusCache(); + $this->assertSame('{"test":1}', $status['lazyCache']['feed']['array']); + } + + public function testSetLazyValueArrayDatabase(): void { + $config = $this->generateAppConfig(); + $config->setValueArray('feed', 'array', ['test' => 1], true); + $config->clearCache(); + $this->assertSame(['test' => 1], $config->getValueArray('feed', 'array', [], true)); + } + + public function testSetLazyValueArrayAsNonLazy(): void { + $config = $this->generateAppConfig(); + $config->setValueArray('feed', 'array', ['test' => 1], true); + $config->setValueArray('feed', 'array', ['test' => 1], false); + $this->assertSame(['test' => 1], $config->getValueArray('feed', 'array', [])); + } + + public function testSetNonLazyValueArrayAsLazy(): void { + $config = $this->generateAppConfig(); + $config->setValueArray('feed', 'array', ['test' => 1], false); + $config->setValueArray('feed', 'array', ['test' => 1], true); + $this->assertSame(['test' => 1], $config->getValueArray('feed', 'array', [], true)); + } + + + public function testSetSensitiveValueArray(): void { + $config = $this->generateAppConfig(); + $config->setValueArray('feed', 'array', ['test' => 1], sensitive: true); + $this->assertEqualsCanonicalizing(['test' => 1], $config->getValueArray('feed', 'array', [])); + } + + public function testSetSensitiveValueArrayCache(): void { + $config = $this->generateAppConfig(); + $config->setValueArray('feed', 'array', ['test' => 1], sensitive: true); + $status = $config->statusCache(); + $this->assertStringStartsWith(self::invokePrivate(AppConfig::class, 'ENCRYPTION_PREFIX'), $status['fastCache']['feed']['array']); + } + + public function testSetSensitiveValueArrayDatabase(): void { + $config = $this->generateAppConfig(); + $config->setValueArray('feed', 'array', ['test' => 1], sensitive: true); + $config->clearCache(); + $this->assertEqualsCanonicalizing(['test' => 1], $config->getValueArray('feed', 'array', [])); + } + + public function testSetNonSensitiveValueArrayAsSensitive(): void { + $config = $this->generateAppConfig(); + $config->setValueArray('feed', 'array', ['test' => 1]); + $config->setValueArray('feed', 'array', ['test' => 1], sensitive: true); + $this->assertSame(true, $config->isSensitive('feed', 'array')); + } + + public function testSetSensitiveValueArrayAsNonSensitiveStaysSensitive(): void { + $config = $this->generateAppConfig(); + $config->setValueArray('feed', 'array', ['test' => 1], sensitive: true); + $config->setValueArray('feed', 'array', ['test' => 2]); + $this->assertSame(true, $config->isSensitive('feed', 'array')); + } + + public function testSetSensitiveValueArrayAsNonSensitiveAreStillUpdated(): void { + $config = $this->generateAppConfig(); + $config->setValueArray('feed', 'array', ['test' => 1], sensitive: true); + $config->setValueArray('feed', 'array', ['test' => 2]); + $this->assertEqualsCanonicalizing(['test' => 2], $config->getValueArray('feed', 'array', [])); + } + + public function testUpdateNotSensitiveToSensitive(): void { + $config = $this->generateAppConfig(); + $config->updateSensitive('non-sensitive-app', 'lazy-key', true); + $this->assertSame(true, $config->isSensitive('non-sensitive-app', 'lazy-key', true)); + } + + public function testUpdateSensitiveToNotSensitive(): void { + $config = $this->generateAppConfig(); + $config->updateSensitive('sensitive-app', 'lazy-key', false); + $this->assertSame(false, $config->isSensitive('sensitive-app', 'lazy-key', true)); + } + + public function testUpdateSensitiveToSensitiveReturnsFalse(): void { + $config = $this->generateAppConfig(); + $this->assertSame(false, $config->updateSensitive('sensitive-app', 'lazy-key', true)); + } + + public function testUpdateNotSensitiveToNotSensitiveReturnsFalse(): void { + $config = $this->generateAppConfig(); + $this->assertSame(false, $config->updateSensitive('non-sensitive-app', 'lazy-key', false)); + } + + public function testUpdateSensitiveOnUnknownKeyReturnsFalse(): void { + $config = $this->generateAppConfig(); + $this->assertSame(false, $config->updateSensitive('non-sensitive-app', 'unknown-key', true)); + } + + public function testUpdateNotLazyToLazy(): void { + $config = $this->generateAppConfig(); + $config->updateLazy('non-sensitive-app', 'non-lazy-key', true); + $this->assertSame(true, $config->isLazy('non-sensitive-app', 'non-lazy-key')); + } + + public function testUpdateLazyToNotLazy(): void { + $config = $this->generateAppConfig(); + $config->updateLazy('non-sensitive-app', 'lazy-key', false); + $this->assertSame(false, $config->isLazy('non-sensitive-app', 'lazy-key')); + } + + public function testUpdateLazyToLazyReturnsFalse(): void { + $config = $this->generateAppConfig(); + $this->assertSame(false, $config->updateLazy('non-sensitive-app', 'lazy-key', true)); + } + + public function testUpdateNotLazyToNotLazyReturnsFalse(): void { + $config = $this->generateAppConfig(); + $this->assertSame(false, $config->updateLazy('non-sensitive-app', 'non-lazy-key', false)); + } + + public function testUpdateLazyOnUnknownKeyReturnsFalse(): void { + $config = $this->generateAppConfig(); + $this->assertSame(false, $config->updateLazy('non-sensitive-app', 'unknown-key', true)); + } + + public function testGetDetails(): void { + $config = $this->generateAppConfig(); + $this->assertEquals( + [ + 'app' => 'non-sensitive-app', + 'key' => 'lazy-key', + 'value' => 'value', + 'type' => 4, + 'lazy' => true, + 'typeString' => 'string', + 'sensitive' => false, + ], + $config->getDetails('non-sensitive-app', 'lazy-key') + ); + } + + public function testGetDetailsSensitive(): void { + $config = $this->generateAppConfig(); + $this->assertEquals( + [ + 'app' => 'sensitive-app', + 'key' => 'lazy-key', + 'value' => 'value', + 'type' => 4, + 'lazy' => true, + 'typeString' => 'string', + 'sensitive' => true, + ], + $config->getDetails('sensitive-app', 'lazy-key') + ); + } + + public function testGetDetailsInt(): void { + $config = $this->generateAppConfig(); + $this->assertEquals( + [ + 'app' => 'typed', + 'key' => 'int', + 'value' => '42', + 'type' => 8, + 'lazy' => false, + 'typeString' => 'integer', + 'sensitive' => false + ], + $config->getDetails('typed', 'int') + ); + } + + public function testGetDetailsFloat(): void { + $config = $this->generateAppConfig(); + $this->assertEquals( + [ + 'app' => 'typed', + 'key' => 'float', + 'value' => '3.14', + 'type' => 16, + 'lazy' => false, + 'typeString' => 'float', + 'sensitive' => false + ], + $config->getDetails('typed', 'float') + ); + } + + public function testGetDetailsBool(): void { + $config = $this->generateAppConfig(); + $this->assertEquals( + [ + 'app' => 'typed', + 'key' => 'bool', + 'value' => '1', + 'type' => 32, + 'lazy' => false, + 'typeString' => 'boolean', + 'sensitive' => false + ], + $config->getDetails('typed', 'bool') + ); + } + + public function testGetDetailsArray(): void { + $config = $this->generateAppConfig(); + $this->assertEquals( + [ + 'app' => 'typed', + 'key' => 'array', + 'value' => '{"test": 1}', + 'type' => 64, + 'lazy' => false, + 'typeString' => 'array', + 'sensitive' => false + ], + $config->getDetails('typed', 'array') + ); + } + + public function testDeleteKey(): void { + $config = $this->generateAppConfig(); + $config->deleteKey('anotherapp', 'key'); + $this->assertSame('default', $config->getValueString('anotherapp', 'key', 'default')); + } + + public function testDeleteKeyCache(): void { + $config = $this->generateAppConfig(); + $config->deleteKey('anotherapp', 'key'); + $status = $config->statusCache(); + $this->assertEqualsCanonicalizing(['enabled' => 'no', 'installed_version' => '3.2.1'], $status['fastCache']['anotherapp']); + } + + public function testDeleteKeyDatabase(): void { + $config = $this->generateAppConfig(); + $config->deleteKey('anotherapp', 'key'); + $config->clearCache(); + $this->assertSame('default', $config->getValueString('anotherapp', 'key', 'default')); + } + + public function testDeleteApp(): void { + $config = $this->generateAppConfig(); + $config->deleteApp('anotherapp'); + $this->assertSame('default', $config->getValueString('anotherapp', 'key', 'default')); + $this->assertSame('default', $config->getValueString('anotherapp', 'enabled', 'default')); + } + + public function testDeleteAppCache(): void { + $config = $this->generateAppConfig(); + $status = $config->statusCache(); + $this->assertSame(true, isset($status['fastCache']['anotherapp'])); + $config->deleteApp('anotherapp'); + $status = $config->statusCache(); + $this->assertSame(false, isset($status['fastCache']['anotherapp'])); + } + + public function testDeleteAppDatabase(): void { + $config = $this->generateAppConfig(); + $config->deleteApp('anotherapp'); + $config->clearCache(); + $this->assertSame('default', $config->getValueString('anotherapp', 'key', 'default')); + $this->assertSame('default', $config->getValueString('anotherapp', 'enabled', 'default')); + } + + public function testClearCache(): void { + $config = $this->generateAppConfig(); + $config->setValueString('feed', 'string', '123454'); + $config->clearCache(); + $status = $config->statusCache(); + $this->assertSame([], $status['fastCache']); + } + + public function testSensitiveValuesAreEncrypted(): void { + $key = self::getUniqueID('secret'); + + $appConfig = $this->generateAppConfig(); + $secret = md5((string)time()); + $appConfig->setValueString('testapp', $key, $secret, sensitive: true); + + $this->assertConfigValueNotEquals('testapp', $key, $secret); + + // Can get in same run + $actualSecret = $appConfig->getValueString('testapp', $key); + $this->assertEquals($secret, $actualSecret); + + // Can get freshly decrypted from DB + $newAppConfig = $this->generateAppConfig(); + $actualSecret = $newAppConfig->getValueString('testapp', $key); + $this->assertEquals($secret, $actualSecret); + } + + public function testMigratingNonSensitiveValueToSensitiveWithSetValue(): void { + $key = self::getUniqueID('secret'); + $appConfig = $this->generateAppConfig(); + $secret = sha1((string)time()); + + // Unencrypted + $appConfig->setValueString('testapp', $key, $secret); + $this->assertConfigKey('testapp', $key, $secret); + + // Can get freshly decrypted from DB + $newAppConfig = $this->generateAppConfig(); + $actualSecret = $newAppConfig->getValueString('testapp', $key); + $this->assertEquals($secret, $actualSecret); + + // Encrypting on change + $appConfig->setValueString('testapp', $key, $secret, sensitive: true); + $this->assertConfigValueNotEquals('testapp', $key, $secret); + + // Can get in same run + $actualSecret = $appConfig->getValueString('testapp', $key); + $this->assertEquals($secret, $actualSecret); + + // Can get freshly decrypted from DB + $newAppConfig = $this->generateAppConfig(); + $actualSecret = $newAppConfig->getValueString('testapp', $key); + $this->assertEquals($secret, $actualSecret); + } + + public function testUpdateSensitiveValueToNonSensitiveWithUpdateSensitive(): void { + $key = self::getUniqueID('secret'); + $appConfig = $this->generateAppConfig(); + $secret = sha1((string)time()); + + // Encrypted + $appConfig->setValueString('testapp', $key, $secret, sensitive: true); + $this->assertConfigValueNotEquals('testapp', $key, $secret); + + // Migrate to non-sensitive / non-encrypted + $appConfig->updateSensitive('testapp', $key, false); + $this->assertConfigKey('testapp', $key, $secret); + } + + public function testUpdateNonSensitiveValueToSensitiveWithUpdateSensitive(): void { + $key = self::getUniqueID('secret'); + $appConfig = $this->generateAppConfig(); + $secret = sha1((string)time()); + + // Unencrypted + $appConfig->setValueString('testapp', $key, $secret); + $this->assertConfigKey('testapp', $key, $secret); + + // Migrate to sensitive / encrypted + $appConfig->updateSensitive('testapp', $key, true); + $this->assertConfigValueNotEquals('testapp', $key, $secret); + } + + public function testSearchKeyNoLazyLoading(): void { + $appConfig = $this->generateAppConfig(); + $appConfig->searchKeys('searchtest', 'search_'); + $status = $appConfig->statusCache(); + $this->assertFalse($status['lazyLoaded'], 'searchKeys() loaded lazy config'); + } + + public function testSearchKeyFast(): void { + $appConfig = $this->generateAppConfig(); + $this->assertEquals(['search_key1', 'search_key2', 'search_key3'], $appConfig->searchKeys('searchtest', 'search_')); + } + + public function testSearchKeyLazy(): void { + $appConfig = $this->generateAppConfig(); + $this->assertEquals(['search_key5_lazy'], $appConfig->searchKeys('searchtest', 'search_', true)); + } + + protected function loadConfigValueFromDatabase(string $app, string $key): string|false { + $sql = $this->connection->getQueryBuilder(); + $sql->select('configvalue') + ->from('appconfig') + ->where($sql->expr()->eq('appid', $sql->createParameter('appid'))) + ->andWhere($sql->expr()->eq('configkey', $sql->createParameter('configkey'))) + ->setParameter('appid', $app) + ->setParameter('configkey', $key); + $query = $sql->executeQuery(); + $actual = $query->fetchOne(); + $query->closeCursor(); + + return $actual; + } + + protected function assertConfigKey(string $app, string $key, string|false $expected): void { + $this->assertEquals($expected, $this->loadConfigValueFromDatabase($app, $key)); + } + + protected function assertConfigValueNotEquals(string $app, string $key, string|false $expected): void { + $this->assertNotEquals($expected, $this->loadConfigValueFromDatabase($app, $key)); + } +} diff --git a/tests/lib/AppConfigTest.php b/tests/lib/AppConfigTest.php index 0ae917a1d9fdd..f5e385a8405e3 100644 --- a/tests/lib/AppConfigTest.php +++ b/tests/lib/AppConfigTest.php @@ -2,1510 +2,203 @@ declare(strict_types=1); /** - * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-only + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace Test; -use InvalidArgumentException; use OC\AppConfig; use OC\Config\ConfigManager; use OC\Config\PresetManager; -use OCP\Exceptions\AppConfigTypeConflictException; -use OCP\Exceptions\AppConfigUnknownKeyException; -use OCP\IAppConfig; +use OC\Memcache\Factory as CacheFactory; +use OCP\DB\IResult; +use OCP\DB\QueryBuilder\IExpressionBuilder; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\ICache; use OCP\IConfig; use OCP\IDBConnection; use OCP\Security\ICrypto; -use OCP\Server; +use PHPUnit\Framework\MockObject\MockObject; use Psr\Log\LoggerInterface; /** * Class AppConfigTest * - * @group DB - * * @package Test */ class AppConfigTest extends TestCase { - protected IAppConfig $appConfig; - protected IDBConnection $connection; - private IConfig $config; - private ConfigManager $configManager; - private PresetManager $presetManager; - private LoggerInterface $logger; - private ICrypto $crypto; - - private array $originalConfig; - - /** - * @var array>> - * [appId => [configKey, configValue, valueType, lazy, sensitive]] - */ - private static array $baseStruct - = [ - 'testapp' => [ - 'enabled' => ['enabled', 'yes'], - 'installed_version' => ['installed_version', '1.2.3'], - 'depends_on' => ['depends_on', 'someapp'], - 'deletethis' => ['deletethis', 'deletethis'], - 'key' => ['key', 'value'] - ], - 'searchtest' => [ - 'search_key1' => ['search_key1', 'key1', IAppConfig::VALUE_STRING], - 'search_key2' => ['search_key2', 'key2', IAppConfig::VALUE_STRING], - 'search_key3' => ['search_key3', 'key3', IAppConfig::VALUE_STRING], - 'searchnot_key4' => ['searchnot_key4', 'key4', IAppConfig::VALUE_STRING], - 'search_key5_lazy' => ['search_key5_lazy', 'key5', IAppConfig::VALUE_STRING, true], - ], - 'someapp' => [ - 'key' => ['key', 'value'], - 'otherkey' => ['otherkey', 'othervalue'] - ], - '123456' => [ - 'enabled' => ['enabled', 'yes'], - 'key' => ['key', 'value'] - ], - 'anotherapp' => [ - 'enabled' => ['enabled', 'no'], - 'installed_version' => ['installed_version', '3.2.1'], - 'key' => ['key', 'value'] - ], - 'non-sensitive-app' => [ - 'lazy-key' => ['lazy-key', 'value', IAppConfig::VALUE_STRING, true, false], - 'non-lazy-key' => ['non-lazy-key', 'value', IAppConfig::VALUE_STRING, false, false], - ], - 'sensitive-app' => [ - 'lazy-key' => ['lazy-key', 'value', IAppConfig::VALUE_STRING, true, true], - 'non-lazy-key' => ['non-lazy-key', 'value', IAppConfig::VALUE_STRING, false, true], - ], - 'only-lazy' => [ - 'lazy-key' => ['lazy-key', 'value', IAppConfig::VALUE_STRING, true] - ], - 'typed' => [ - 'mixed' => ['mixed', 'mix', IAppConfig::VALUE_MIXED], - 'string' => ['string', 'value', IAppConfig::VALUE_STRING], - 'int' => ['int', '42', IAppConfig::VALUE_INT], - 'float' => ['float', '3.14', IAppConfig::VALUE_FLOAT], - 'bool' => ['bool', '1', IAppConfig::VALUE_BOOL], - 'array' => ['array', '{"test": 1}', IAppConfig::VALUE_ARRAY], - ], - 'prefix-app' => [ - 'key1' => ['key1', 'value'], - 'prefix1' => ['prefix1', 'value'], - 'prefix-2' => ['prefix-2', 'value'], - 'key-2' => ['key-2', 'value'], - ] - ]; + private IConfig&MockObject $config; + private IDBConnection&MockObject $connection; + private ConfigManager&MockObject $configManager; + private PresetManager&MockObject $presetManager; + private LoggerInterface&MockObject $logger; + private ICrypto&MockObject $crypto; + private CacheFactory&MockObject $cacheFactory; + private ICache&MockObject $localCache; protected function setUp(): void { parent::setUp(); - $this->connection = Server::get(IDBConnection::class); - $this->config = Server::get(IConfig::class); - $this->configManager = Server::get(ConfigManager::class); - $this->presetManager = Server::get(PresetManager::class); - $this->logger = Server::get(LoggerInterface::class); - $this->crypto = Server::get(ICrypto::class); - - // storing current config and emptying the data table - $sql = $this->connection->getQueryBuilder(); - $sql->select('*') - ->from('appconfig'); - $result = $sql->executeQuery(); - $this->originalConfig = $result->fetchAll(); - $result->closeCursor(); - - $sql = $this->connection->getQueryBuilder(); - $sql->delete('appconfig'); - $sql->executeStatement(); - - $sql = $this->connection->getQueryBuilder(); - $sql->insert('appconfig') - ->values( - [ - 'appid' => $sql->createParameter('appid'), - 'configkey' => $sql->createParameter('configkey'), - 'configvalue' => $sql->createParameter('configvalue'), - 'type' => $sql->createParameter('type'), - 'lazy' => $sql->createParameter('lazy') - ] - ); - - foreach (self::$baseStruct as $appId => $appData) { - foreach ($appData as $key => $row) { - $value = $row[1]; - $type = $row[2] ?? IAppConfig::VALUE_MIXED; - if (($row[4] ?? false) === true) { - $type |= IAppConfig::VALUE_SENSITIVE; - $value = self::invokePrivate(AppConfig::class, 'ENCRYPTION_PREFIX') . $this->crypto->encrypt($value); - self::$baseStruct[$appId][$key]['encrypted'] = $value; - } - - $sql->setParameters( - [ - 'appid' => $appId, - 'configkey' => $row[0], - 'configvalue' => $value, - 'type' => $type, - 'lazy' => (($row[3] ?? false) === true) ? 1 : 0 - ] - )->executeStatement(); - } - } - } - - protected function tearDown(): void { - $sql = $this->connection->getQueryBuilder(); - $sql->delete('appconfig'); - $sql->executeStatement(); - - $sql = $this->connection->getQueryBuilder(); - $sql->insert('appconfig') - ->values( - [ - 'appid' => $sql->createParameter('appid'), - 'configkey' => $sql->createParameter('configkey'), - 'configvalue' => $sql->createParameter('configvalue'), - 'lazy' => $sql->createParameter('lazy'), - 'type' => $sql->createParameter('type'), - ] - ); - - foreach ($this->originalConfig as $key => $configs) { - $sql->setParameter('appid', $configs['appid']) - ->setParameter('configkey', $configs['configkey']) - ->setParameter('configvalue', $configs['configvalue']) - ->setParameter('lazy', ($configs['lazy'] === '1') ? '1' : '0') - ->setParameter('type', $configs['type']); - $sql->executeStatement(); + $this->connection = $this->createMock(IDBConnection::class); + $this->config = $this->createMock(IConfig::class); + $this->configManager = $this->createMock(ConfigManager::class); + $this->presetManager = $this->createMock(PresetManager::class); + $this->logger = $this->createMock(LoggerInterface::class); + $this->crypto = $this->createMock(ICrypto::class); + $this->cacheFactory = $this->createMock(CacheFactory::class); + $this->localCache = $this->createMock(ICache::class); + } + + protected function getAppConfig($cached = false): AppConfig { + $this->config->method('getSystemValueBool') + ->with('cache_app_config', $cached) + ->willReturn(true); + $this->cacheFactory->method('isLocalCacheAvailable')->willReturn($cached); + if ($cached) { + $this->cacheFactory->method('withServerVersionPrefix')->willReturnCallback(function (\Closure $closure): void { + $closure($this->cacheFactory); + }); + $this->cacheFactory->method('createLocal')->willReturn($this->localCache); } - // $this->restoreService(AppConfig::class); - parent::tearDown(); - } - - /** - * @param bool $preLoading TRUE will preload the 'fast' cache, which is the normal behavior of usual - * IAppConfig - * - * @return IAppConfig - */ - private function generateAppConfig(bool $preLoading = true): IAppConfig { - /** @var AppConfig $config */ - $config = new AppConfig( + return new AppConfig( $this->connection, $this->config, $this->configManager, $this->presetManager, $this->logger, $this->crypto, + $this->cacheFactory, ); - $msg = ' generateAppConfig() failed to confirm cache status'; - - // confirm cache status - $status = $config->statusCache(); - $this->assertSame(false, $status['fastLoaded'], $msg); - $this->assertSame(false, $status['lazyLoaded'], $msg); - $this->assertSame([], $status['fastCache'], $msg); - $this->assertSame([], $status['lazyCache'], $msg); - if ($preLoading) { - // simple way to initiate the load of non-lazy config values in cache - $config->getValueString('core', 'preload', ''); - - // confirm cache status - $status = $config->statusCache(); - $this->assertSame(true, $status['fastLoaded'], $msg); - $this->assertSame(false, $status['lazyLoaded'], $msg); - - $apps = array_values(array_diff(array_keys(self::$baseStruct), ['only-lazy'])); - $this->assertEqualsCanonicalizing($apps, array_keys($status['fastCache']), $msg); - $this->assertSame([], array_keys($status['lazyCache']), $msg); - } - - return $config; - } - - public function testGetApps(): void { - $config = $this->generateAppConfig(false); - - $this->assertEqualsCanonicalizing(array_keys(self::$baseStruct), $config->getApps()); - } - - public function testGetAppInstalledVersions(): void { - $config = $this->generateAppConfig(false); - - $this->assertEquals( - ['testapp' => '1.2.3', 'anotherapp' => '3.2.1'], - $config->getAppInstalledVersions(false) - ); - $this->assertEquals( - ['testapp' => '1.2.3'], - $config->getAppInstalledVersions(true) - ); - } - - /** - * returns list of app and their keys - * - * @return array ['appId' => ['key1', 'key2', ]] - * @see testGetKeys - */ - public static function providerGetAppKeys(): array { - $appKeys = []; - foreach (self::$baseStruct as $appId => $appData) { - $keys = []; - foreach ($appData as $row) { - $keys[] = $row[0]; - } - $appKeys[] = [(string)$appId, $keys]; - } - - return $appKeys; - } - - /** - * returns list of config keys - * - * @return array [appId, key, value, type, lazy, sensitive] - * @see testIsSensitive - * @see testIsLazy - * @see testGetKeys - */ - public static function providerGetKeys(): array { - $appKeys = []; - foreach (self::$baseStruct as $appId => $appData) { - foreach ($appData as $row) { - $appKeys[] = [ - (string)$appId, $row[0], $row[1], $row[2] ?? IAppConfig::VALUE_MIXED, $row[3] ?? false, - $row[4] ?? false - ]; - } - } - - return $appKeys; - } - - /** - * - * @param string $appId - * @param array $expectedKeys - */ - #[\PHPUnit\Framework\Attributes\DataProvider('providerGetAppKeys')] - public function testGetKeys(string $appId, array $expectedKeys): void { - $config = $this->generateAppConfig(); - $this->assertEqualsCanonicalizing($expectedKeys, $config->getKeys($appId)); - } - - public function testGetKeysOnUnknownAppShouldReturnsEmptyArray(): void { - $config = $this->generateAppConfig(); - $this->assertEqualsCanonicalizing([], $config->getKeys('unknown-app')); - } - - /** - * - * @param string $appId - * @param string $configKey - * @param string $value - * @param bool $lazy - */ - #[\PHPUnit\Framework\Attributes\DataProvider('providerGetKeys')] - public function testHasKey(string $appId, string $configKey, string $value, int $type, bool $lazy): void { - $config = $this->generateAppConfig(); - $this->assertEquals(true, $config->hasKey($appId, $configKey, $lazy)); - } - - public function testHasKeyOnNonExistentKeyReturnsFalse(): void { - $config = $this->generateAppConfig(); - $this->assertEquals(false, $config->hasKey(array_keys(self::$baseStruct)[0], 'inexistant-key')); - } - - public function testHasKeyOnUnknownAppReturnsFalse(): void { - $config = $this->generateAppConfig(); - $this->assertEquals(false, $config->hasKey('inexistant-app', 'inexistant-key')); - } - - public function testHasKeyOnMistypedAsLazyReturnsFalse(): void { - $config = $this->generateAppConfig(); - $this->assertSame(false, $config->hasKey('non-sensitive-app', 'non-lazy-key', true)); - } - - public function testHasKeyOnMistypeAsNonLazyReturnsFalse(): void { - $config = $this->generateAppConfig(); - $this->assertSame(false, $config->hasKey('non-sensitive-app', 'lazy-key', false)); - } - - public function testHasKeyOnMistypeAsNonLazyReturnsTrueWithLazyArgumentIsNull(): void { - $config = $this->generateAppConfig(); - $this->assertSame(true, $config->hasKey('non-sensitive-app', 'lazy-key', null)); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('providerGetKeys')] - public function testIsSensitive( - string $appId, string $configKey, string $configValue, int $type, bool $lazy, bool $sensitive, - ): void { - $config = $this->generateAppConfig(); - $this->assertEquals($sensitive, $config->isSensitive($appId, $configKey, $lazy)); - } - - public function testIsSensitiveOnNonExistentKeyThrowsException(): void { - $config = $this->generateAppConfig(); - $this->expectException(AppConfigUnknownKeyException::class); - $config->isSensitive(array_keys(self::$baseStruct)[0], 'inexistant-key'); - } - - public function testIsSensitiveOnUnknownAppThrowsException(): void { - $config = $this->generateAppConfig(); - $this->expectException(AppConfigUnknownKeyException::class); - $config->isSensitive('unknown-app', 'inexistant-key'); - } - - public function testIsSensitiveOnSensitiveMistypedAsLazy(): void { - $config = $this->generateAppConfig(); - $this->assertSame(true, $config->isSensitive('sensitive-app', 'non-lazy-key', true)); - } - - public function testIsSensitiveOnNonSensitiveMistypedAsLazy(): void { - $config = $this->generateAppConfig(); - $this->assertSame(false, $config->isSensitive('non-sensitive-app', 'non-lazy-key', true)); - } - - public function testIsSensitiveOnSensitiveMistypedAsNonLazyThrowsException(): void { - $config = $this->generateAppConfig(); - $this->expectException(AppConfigUnknownKeyException::class); - $config->isSensitive('sensitive-app', 'lazy-key', false); - } - - public function testIsSensitiveOnNonSensitiveMistypedAsNonLazyThrowsException(): void { - $config = $this->generateAppConfig(); - $this->expectException(AppConfigUnknownKeyException::class); - $config->isSensitive('non-sensitive-app', 'lazy-key', false); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('providerGetKeys')] - public function testIsLazy(string $appId, string $configKey, string $configValue, int $type, bool $lazy, - ): void { - $config = $this->generateAppConfig(); - $this->assertEquals($lazy, $config->isLazy($appId, $configKey)); - } - - public function testIsLazyOnNonExistentKeyThrowsException(): void { - $config = $this->generateAppConfig(); - $this->expectException(AppConfigUnknownKeyException::class); - $config->isLazy(array_keys(self::$baseStruct)[0], 'inexistant-key'); - } - - public function testIsLazyOnUnknownAppThrowsException(): void { - $config = $this->generateAppConfig(); - $this->expectException(AppConfigUnknownKeyException::class); - $config->isLazy('unknown-app', 'inexistant-key'); - } - - public function testGetAllValues(): void { - $config = $this->generateAppConfig(); - $this->assertEquals( - [ - 'array' => ['test' => 1], - 'bool' => true, - 'float' => 3.14, - 'int' => 42, - 'mixed' => 'mix', - 'string' => 'value', - ], - $config->getAllValues('typed') - ); - } - - public function testGetAllValuesWithEmptyApp(): void { - $config = $this->generateAppConfig(); - $this->expectException(InvalidArgumentException::class); - $config->getAllValues(''); - } - - /** - * - * @param string $appId - * @param array $keys - */ - #[\PHPUnit\Framework\Attributes\DataProvider('providerGetAppKeys')] - public function testGetAllValuesWithEmptyKey(string $appId, array $keys): void { - $config = $this->generateAppConfig(); - $this->assertEqualsCanonicalizing($keys, array_keys($config->getAllValues($appId, ''))); - } - - public function testGetAllValuesWithPrefix(): void { - $config = $this->generateAppConfig(); - $this->assertEqualsCanonicalizing(['prefix1', 'prefix-2'], array_keys($config->getAllValues('prefix-app', 'prefix'))); - } - - public function testSearchValues(): void { - $config = $this->generateAppConfig(); - $this->assertEqualsCanonicalizing(['testapp' => 'yes', '123456' => 'yes', 'anotherapp' => 'no'], $config->searchValues('enabled')); - } - - public function testGetValueString(): void { - $config = $this->generateAppConfig(); - $this->assertSame('value', $config->getValueString('typed', 'string', '')); - } - - public function testGetValueStringOnUnknownAppReturnsDefault(): void { - $config = $this->generateAppConfig(); - $this->assertSame('default-1', $config->getValueString('typed-1', 'string', 'default-1')); - } - - public function testGetValueStringOnNonExistentKeyReturnsDefault(): void { - $config = $this->generateAppConfig(); - $this->assertSame('default-2', $config->getValueString('typed', 'string-2', 'default-2')); - } - - public function testGetValueStringOnWrongType(): void { - $config = $this->generateAppConfig(); - $this->expectException(AppConfigTypeConflictException::class); - $config->getValueString('typed', 'int'); - } - - public function testGetNonLazyValueStringAsLazy(): void { - $config = $this->generateAppConfig(); - $this->assertSame('value', $config->getValueString('non-sensitive-app', 'non-lazy-key', 'default', lazy: true)); - } - - public function testGetValueInt(): void { - $config = $this->generateAppConfig(); - $this->assertSame(42, $config->getValueInt('typed', 'int', 0)); - } - - public function testGetValueIntOnUnknownAppReturnsDefault(): void { - $config = $this->generateAppConfig(); - $this->assertSame(1, $config->getValueInt('typed-1', 'int', 1)); - } - - public function testGetValueIntOnNonExistentKeyReturnsDefault(): void { - $config = $this->generateAppConfig(); - $this->assertSame(2, $config->getValueInt('typed', 'int-2', 2)); - } - - public function testGetValueIntOnWrongType(): void { - $config = $this->generateAppConfig(); - $this->expectException(AppConfigTypeConflictException::class); - $config->getValueInt('typed', 'float'); - } - - public function testGetValueFloat(): void { - $config = $this->generateAppConfig(); - $this->assertSame(3.14, $config->getValueFloat('typed', 'float', 0)); - } - - public function testGetValueFloatOnNonUnknownAppReturnsDefault(): void { - $config = $this->generateAppConfig(); - $this->assertSame(1.11, $config->getValueFloat('typed-1', 'float', 1.11)); - } - - public function testGetValueFloatOnNonExistentKeyReturnsDefault(): void { - $config = $this->generateAppConfig(); - $this->assertSame(2.22, $config->getValueFloat('typed', 'float-2', 2.22)); - } - - public function testGetValueFloatOnWrongType(): void { - $config = $this->generateAppConfig(); - $this->expectException(AppConfigTypeConflictException::class); - $config->getValueFloat('typed', 'bool'); - } - - public function testGetValueBool(): void { - $config = $this->generateAppConfig(); - $this->assertSame(true, $config->getValueBool('typed', 'bool')); - } - - public function testGetValueBoolOnUnknownAppReturnsDefault(): void { - $config = $this->generateAppConfig(); - $this->assertSame(false, $config->getValueBool('typed-1', 'bool', false)); - } - - public function testGetValueBoolOnNonExistentKeyReturnsDefault(): void { - $config = $this->generateAppConfig(); - $this->assertSame(false, $config->getValueBool('typed', 'bool-2')); - } - - public function testGetValueBoolOnWrongType(): void { - $config = $this->generateAppConfig(); - $this->expectException(AppConfigTypeConflictException::class); - $config->getValueBool('typed', 'array'); - } - - public function testGetValueArray(): void { - $config = $this->generateAppConfig(); - $this->assertEqualsCanonicalizing(['test' => 1], $config->getValueArray('typed', 'array', [])); - } - - public function testGetValueArrayOnUnknownAppReturnsDefault(): void { - $config = $this->generateAppConfig(); - $this->assertSame([1], $config->getValueArray('typed-1', 'array', [1])); - } - - public function testGetValueArrayOnNonExistentKeyReturnsDefault(): void { - $config = $this->generateAppConfig(); - $this->assertSame([1, 2], $config->getValueArray('typed', 'array-2', [1, 2])); - } - - public function testGetValueArrayOnWrongType(): void { - $config = $this->generateAppConfig(); - $this->expectException(AppConfigTypeConflictException::class); - $config->getValueArray('typed', 'string'); - } - - - /** - * @return array - * @see testGetValueType - * - * @see testGetValueMixed - */ - public static function providerGetValueMixed(): array { - return [ - // key, value, type - ['mixed', 'mix', IAppConfig::VALUE_MIXED], - ['string', 'value', IAppConfig::VALUE_STRING], - ['int', '42', IAppConfig::VALUE_INT], - ['float', '3.14', IAppConfig::VALUE_FLOAT], - ['bool', '1', IAppConfig::VALUE_BOOL], - ['array', '{"test": 1}', IAppConfig::VALUE_ARRAY], - ]; - } - - /** - * - * @param string $key - * @param string $value - */ - #[\PHPUnit\Framework\Attributes\DataProvider('providerGetValueMixed')] - public function testGetValueMixed(string $key, string $value): void { - $config = $this->generateAppConfig(); - $this->assertSame($value, $config->getValueMixed('typed', $key)); - } - - /** - * - * @param string $key - * @param string $value - * @param int $type - */ - #[\PHPUnit\Framework\Attributes\DataProvider('providerGetValueMixed')] - public function testGetValueType(string $key, string $value, int $type): void { - $config = $this->generateAppConfig(); - $this->assertSame($type, $config->getValueType('typed', $key)); - } - - public function testGetValueTypeOnUnknownApp(): void { - $config = $this->generateAppConfig(); - $this->expectException(AppConfigUnknownKeyException::class); - $config->getValueType('typed-1', 'string'); - } - - public function testGetValueTypeOnNonExistentKey(): void { - $config = $this->generateAppConfig(); - $this->expectException(AppConfigUnknownKeyException::class); - $config->getValueType('typed', 'string-2'); - } - - public function testSetValueString(): void { - $config = $this->generateAppConfig(); - $config->setValueString('feed', 'string', 'value-1'); - $this->assertSame('value-1', $config->getValueString('feed', 'string', '')); - } - - public function testSetValueStringCache(): void { - $config = $this->generateAppConfig(); - $config->setValueString('feed', 'string', 'value-1'); - $status = $config->statusCache(); - $this->assertSame('value-1', $status['fastCache']['feed']['string']); - } - - public function testSetValueStringDatabase(): void { - $config = $this->generateAppConfig(); - $config->setValueString('feed', 'string', 'value-1'); - $config->clearCache(); - $this->assertSame('value-1', $config->getValueString('feed', 'string', '')); - } - - public function testSetValueStringIsUpdated(): void { - $config = $this->generateAppConfig(); - $config->setValueString('feed', 'string', 'value-1'); - $this->assertSame(true, $config->setValueString('feed', 'string', 'value-2')); - } - - public function testSetValueStringIsNotUpdated(): void { - $config = $this->generateAppConfig(); - $config->setValueString('feed', 'string', 'value-1'); - $this->assertSame(false, $config->setValueString('feed', 'string', 'value-1')); - } - - public function testSetValueStringIsUpdatedCache(): void { - $config = $this->generateAppConfig(); - $config->setValueString('feed', 'string', 'value-1'); - $config->setValueString('feed', 'string', 'value-2'); - $status = $config->statusCache(); - $this->assertSame('value-2', $status['fastCache']['feed']['string']); - } - - public function testSetValueStringIsUpdatedDatabase(): void { - $config = $this->generateAppConfig(); - $config->setValueString('feed', 'string', 'value-1'); - $config->setValueString('feed', 'string', 'value-2'); - $config->clearCache(); - $this->assertSame('value-2', $config->getValueString('feed', 'string', '')); - } - - public function testSetValueInt(): void { - $config = $this->generateAppConfig(); - $config->setValueInt('feed', 'int', 42); - $this->assertSame(42, $config->getValueInt('feed', 'int', 0)); - } - - public function testSetValueIntCache(): void { - $config = $this->generateAppConfig(); - $config->setValueInt('feed', 'int', 42); - $status = $config->statusCache(); - $this->assertSame('42', $status['fastCache']['feed']['int']); - } - - public function testSetValueIntDatabase(): void { - $config = $this->generateAppConfig(); - $config->setValueInt('feed', 'int', 42); - $config->clearCache(); - $this->assertSame(42, $config->getValueInt('feed', 'int', 0)); - } - - public function testSetValueIntIsUpdated(): void { - $config = $this->generateAppConfig(); - $config->setValueInt('feed', 'int', 42); - $this->assertSame(true, $config->setValueInt('feed', 'int', 17)); - } - - public function testSetValueIntIsNotUpdated(): void { - $config = $this->generateAppConfig(); - $config->setValueInt('feed', 'int', 42); - $this->assertSame(false, $config->setValueInt('feed', 'int', 42)); - } - - public function testSetValueIntIsUpdatedCache(): void { - $config = $this->generateAppConfig(); - $config->setValueInt('feed', 'int', 42); - $config->setValueInt('feed', 'int', 17); - $status = $config->statusCache(); - $this->assertSame('17', $status['fastCache']['feed']['int']); - } - - public function testSetValueIntIsUpdatedDatabase(): void { - $config = $this->generateAppConfig(); - $config->setValueInt('feed', 'int', 42); - $config->setValueInt('feed', 'int', 17); - $config->clearCache(); - $this->assertSame(17, $config->getValueInt('feed', 'int', 0)); - } - - public function testSetValueFloat(): void { - $config = $this->generateAppConfig(); - $config->setValueFloat('feed', 'float', 3.14); - $this->assertSame(3.14, $config->getValueFloat('feed', 'float', 0)); - } - - public function testSetValueFloatCache(): void { - $config = $this->generateAppConfig(); - $config->setValueFloat('feed', 'float', 3.14); - $status = $config->statusCache(); - $this->assertSame('3.14', $status['fastCache']['feed']['float']); - } - - public function testSetValueFloatDatabase(): void { - $config = $this->generateAppConfig(); - $config->setValueFloat('feed', 'float', 3.14); - $config->clearCache(); - $this->assertSame(3.14, $config->getValueFloat('feed', 'float', 0)); - } - - public function testSetValueFloatIsUpdated(): void { - $config = $this->generateAppConfig(); - $config->setValueFloat('feed', 'float', 3.14); - $this->assertSame(true, $config->setValueFloat('feed', 'float', 1.23)); - } - - public function testSetValueFloatIsNotUpdated(): void { - $config = $this->generateAppConfig(); - $config->setValueFloat('feed', 'float', 3.14); - $this->assertSame(false, $config->setValueFloat('feed', 'float', 3.14)); - } - - public function testSetValueFloatIsUpdatedCache(): void { - $config = $this->generateAppConfig(); - $config->setValueFloat('feed', 'float', 3.14); - $config->setValueFloat('feed', 'float', 1.23); - $status = $config->statusCache(); - $this->assertSame('1.23', $status['fastCache']['feed']['float']); - } - - public function testSetValueFloatIsUpdatedDatabase(): void { - $config = $this->generateAppConfig(); - $config->setValueFloat('feed', 'float', 3.14); - $config->setValueFloat('feed', 'float', 1.23); - $config->clearCache(); - $this->assertSame(1.23, $config->getValueFloat('feed', 'float', 0)); - } - - public function testSetValueBool(): void { - $config = $this->generateAppConfig(); - $config->setValueBool('feed', 'bool', true); - $this->assertSame(true, $config->getValueBool('feed', 'bool', false)); - } - - public function testSetValueBoolCache(): void { - $config = $this->generateAppConfig(); - $config->setValueBool('feed', 'bool', true); - $status = $config->statusCache(); - $this->assertSame('1', $status['fastCache']['feed']['bool']); - } - - public function testSetValueBoolDatabase(): void { - $config = $this->generateAppConfig(); - $config->setValueBool('feed', 'bool', true); - $config->clearCache(); - $this->assertSame(true, $config->getValueBool('feed', 'bool', false)); - } - - public function testSetValueBoolIsUpdated(): void { - $config = $this->generateAppConfig(); - $config->setValueBool('feed', 'bool', true); - $this->assertSame(true, $config->setValueBool('feed', 'bool', false)); - } - - public function testSetValueBoolIsNotUpdated(): void { - $config = $this->generateAppConfig(); - $config->setValueBool('feed', 'bool', true); - $this->assertSame(false, $config->setValueBool('feed', 'bool', true)); - } - - public function testSetValueBoolIsUpdatedCache(): void { - $config = $this->generateAppConfig(); - $config->setValueBool('feed', 'bool', true); - $config->setValueBool('feed', 'bool', false); - $status = $config->statusCache(); - $this->assertSame('0', $status['fastCache']['feed']['bool']); - } - - public function testSetValueBoolIsUpdatedDatabase(): void { - $config = $this->generateAppConfig(); - $config->setValueBool('feed', 'bool', true); - $config->setValueBool('feed', 'bool', false); - $config->clearCache(); - $this->assertSame(false, $config->getValueBool('feed', 'bool', true)); - } - - - public function testSetValueArray(): void { - $config = $this->generateAppConfig(); - $config->setValueArray('feed', 'array', ['test' => 1]); - $this->assertSame(['test' => 1], $config->getValueArray('feed', 'array', [])); - } - - public function testSetValueArrayCache(): void { - $config = $this->generateAppConfig(); - $config->setValueArray('feed', 'array', ['test' => 1]); - $status = $config->statusCache(); - $this->assertSame('{"test":1}', $status['fastCache']['feed']['array']); - } - - public function testSetValueArrayDatabase(): void { - $config = $this->generateAppConfig(); - $config->setValueArray('feed', 'array', ['test' => 1]); - $config->clearCache(); - $this->assertSame(['test' => 1], $config->getValueArray('feed', 'array', [])); - } - - public function testSetValueArrayIsUpdated(): void { - $config = $this->generateAppConfig(); - $config->setValueArray('feed', 'array', ['test' => 1]); - $this->assertSame(true, $config->setValueArray('feed', 'array', ['test' => 2])); - } - - public function testSetValueArrayIsNotUpdated(): void { - $config = $this->generateAppConfig(); - $config->setValueArray('feed', 'array', ['test' => 1]); - $this->assertSame(false, $config->setValueArray('feed', 'array', ['test' => 1])); - } - - public function testSetValueArrayIsUpdatedCache(): void { - $config = $this->generateAppConfig(); - $config->setValueArray('feed', 'array', ['test' => 1]); - $config->setValueArray('feed', 'array', ['test' => 2]); - $status = $config->statusCache(); - $this->assertSame('{"test":2}', $status['fastCache']['feed']['array']); - } - - public function testSetValueArrayIsUpdatedDatabase(): void { - $config = $this->generateAppConfig(); - $config->setValueArray('feed', 'array', ['test' => 1]); - $config->setValueArray('feed', 'array', ['test' => 2]); - $config->clearCache(); - $this->assertSame(['test' => 2], $config->getValueArray('feed', 'array', [])); - } - - public function testSetLazyValueString(): void { - $config = $this->generateAppConfig(); - $config->setValueString('feed', 'string', 'value-1', true); - $this->assertSame('value-1', $config->getValueString('feed', 'string', '', true)); - } - - public function testSetLazyValueStringCache(): void { - $config = $this->generateAppConfig(); - $config->setValueString('feed', 'string', 'value-1', true); - $status = $config->statusCache(); - $this->assertSame('value-1', $status['lazyCache']['feed']['string']); - } - - public function testSetLazyValueStringDatabase(): void { - $config = $this->generateAppConfig(); - $config->setValueString('feed', 'string', 'value-1', true); - $config->clearCache(); - $this->assertSame('value-1', $config->getValueString('feed', 'string', '', true)); - } - - public function testSetLazyValueStringAsNonLazy(): void { - $config = $this->generateAppConfig(); - $config->setValueString('feed', 'string', 'value-1', true); - $config->setValueString('feed', 'string', 'value-1', false); - $this->assertSame('value-1', $config->getValueString('feed', 'string', '')); - } - - public function testSetNonLazyValueStringAsLazy(): void { - $config = $this->generateAppConfig(); - $config->setValueString('feed', 'string', 'value-1', false); - $config->setValueString('feed', 'string', 'value-1', true); - $this->assertSame('value-1', $config->getValueString('feed', 'string', '', true)); - } - - public function testSetSensitiveValueString(): void { - $config = $this->generateAppConfig(); - $config->setValueString('feed', 'string', 'value-1', sensitive: true); - $this->assertSame('value-1', $config->getValueString('feed', 'string', '')); - } - - public function testSetSensitiveValueStringCache(): void { - $config = $this->generateAppConfig(); - $config->setValueString('feed', 'string', 'value-1', sensitive: true); - $status = $config->statusCache(); - $this->assertStringStartsWith(self::invokePrivate(AppConfig::class, 'ENCRYPTION_PREFIX'), $status['fastCache']['feed']['string']); - } - - public function testSetSensitiveValueStringDatabase(): void { - $config = $this->generateAppConfig(); - $config->setValueString('feed', 'string', 'value-1', sensitive: true); - $config->clearCache(); - $this->assertSame('value-1', $config->getValueString('feed', 'string', '')); - } - - public function testSetNonSensitiveValueStringAsSensitive(): void { - $config = $this->generateAppConfig(); - $config->setValueString('feed', 'string', 'value-1', sensitive: false); - $config->setValueString('feed', 'string', 'value-1', sensitive: true); - $this->assertSame(true, $config->isSensitive('feed', 'string')); - - $this->assertConfigValueNotEquals('feed', 'string', 'value-1'); - $this->assertConfigValueNotEquals('feed', 'string', 'value-2'); - } - - public function testSetSensitiveValueStringAsNonSensitiveStaysSensitive(): void { - $config = $this->generateAppConfig(); - $config->setValueString('feed', 'string', 'value-1', sensitive: true); - $config->setValueString('feed', 'string', 'value-2', sensitive: false); - $this->assertSame(true, $config->isSensitive('feed', 'string')); - - $this->assertConfigValueNotEquals('feed', 'string', 'value-1'); - $this->assertConfigValueNotEquals('feed', 'string', 'value-2'); - } - - public function testSetSensitiveValueStringAsNonSensitiveAreStillUpdated(): void { - $config = $this->generateAppConfig(); - $config->setValueString('feed', 'string', 'value-1', sensitive: true); - $config->setValueString('feed', 'string', 'value-2', sensitive: false); - $this->assertSame('value-2', $config->getValueString('feed', 'string', '')); - - $this->assertConfigValueNotEquals('feed', 'string', 'value-1'); - $this->assertConfigValueNotEquals('feed', 'string', 'value-2'); - } - - public function testSetLazyValueInt(): void { - $config = $this->generateAppConfig(); - $config->setValueInt('feed', 'int', 42, true); - $this->assertSame(42, $config->getValueInt('feed', 'int', 0, true)); - } - - public function testSetLazyValueIntCache(): void { - $config = $this->generateAppConfig(); - $config->setValueInt('feed', 'int', 42, true); - $status = $config->statusCache(); - $this->assertSame('42', $status['lazyCache']['feed']['int']); - } - - public function testSetLazyValueIntDatabase(): void { - $config = $this->generateAppConfig(); - $config->setValueInt('feed', 'int', 42, true); - $config->clearCache(); - $this->assertSame(42, $config->getValueInt('feed', 'int', 0, true)); - } - - public function testSetLazyValueIntAsNonLazy(): void { - $config = $this->generateAppConfig(); - $config->setValueInt('feed', 'int', 42, true); - $config->setValueInt('feed', 'int', 42, false); - $this->assertSame(42, $config->getValueInt('feed', 'int', 0)); - } - - public function testSetNonLazyValueIntAsLazy(): void { - $config = $this->generateAppConfig(); - $config->setValueInt('feed', 'int', 42, false); - $config->setValueInt('feed', 'int', 42, true); - $this->assertSame(42, $config->getValueInt('feed', 'int', 0, true)); - } - - public function testSetSensitiveValueInt(): void { - $config = $this->generateAppConfig(); - $config->setValueInt('feed', 'int', 42, sensitive: true); - $this->assertSame(42, $config->getValueInt('feed', 'int', 0)); - } - - public function testSetSensitiveValueIntCache(): void { - $config = $this->generateAppConfig(); - $config->setValueInt('feed', 'int', 42, sensitive: true); - $status = $config->statusCache(); - $this->assertStringStartsWith(self::invokePrivate(AppConfig::class, 'ENCRYPTION_PREFIX'), $status['fastCache']['feed']['int']); - } - - public function testSetSensitiveValueIntDatabase(): void { - $config = $this->generateAppConfig(); - $config->setValueInt('feed', 'int', 42, sensitive: true); - $config->clearCache(); - $this->assertSame(42, $config->getValueInt('feed', 'int', 0)); - } - - public function testSetNonSensitiveValueIntAsSensitive(): void { - $config = $this->generateAppConfig(); - $config->setValueInt('feed', 'int', 42); - $config->setValueInt('feed', 'int', 42, sensitive: true); - $this->assertSame(true, $config->isSensitive('feed', 'int')); - } - - public function testSetSensitiveValueIntAsNonSensitiveStaysSensitive(): void { - $config = $this->generateAppConfig(); - $config->setValueInt('feed', 'int', 42, sensitive: true); - $config->setValueInt('feed', 'int', 17); - $this->assertSame(true, $config->isSensitive('feed', 'int')); - } - - public function testSetSensitiveValueIntAsNonSensitiveAreStillUpdated(): void { - $config = $this->generateAppConfig(); - $config->setValueInt('feed', 'int', 42, sensitive: true); - $config->setValueInt('feed', 'int', 17); - $this->assertSame(17, $config->getValueInt('feed', 'int', 0)); - } - - public function testSetLazyValueFloat(): void { - $config = $this->generateAppConfig(); - $config->setValueFloat('feed', 'float', 3.14, true); - $this->assertSame(3.14, $config->getValueFloat('feed', 'float', 0, true)); - } - - public function testSetLazyValueFloatCache(): void { - $config = $this->generateAppConfig(); - $config->setValueFloat('feed', 'float', 3.14, true); - $status = $config->statusCache(); - $this->assertSame('3.14', $status['lazyCache']['feed']['float']); - } - - public function testSetLazyValueFloatDatabase(): void { - $config = $this->generateAppConfig(); - $config->setValueFloat('feed', 'float', 3.14, true); - $config->clearCache(); - $this->assertSame(3.14, $config->getValueFloat('feed', 'float', 0, true)); - } - - public function testSetLazyValueFloatAsNonLazy(): void { - $config = $this->generateAppConfig(); - $config->setValueFloat('feed', 'float', 3.14, true); - $config->setValueFloat('feed', 'float', 3.14, false); - $this->assertSame(3.14, $config->getValueFloat('feed', 'float', 0)); - } - - public function testSetNonLazyValueFloatAsLazy(): void { - $config = $this->generateAppConfig(); - $config->setValueFloat('feed', 'float', 3.14, false); - $config->setValueFloat('feed', 'float', 3.14, true); - $this->assertSame(3.14, $config->getValueFloat('feed', 'float', 0, true)); - } - - public function testSetSensitiveValueFloat(): void { - $config = $this->generateAppConfig(); - $config->setValueFloat('feed', 'float', 3.14, sensitive: true); - $this->assertSame(3.14, $config->getValueFloat('feed', 'float', 0)); - } - - public function testSetSensitiveValueFloatCache(): void { - $config = $this->generateAppConfig(); - $config->setValueFloat('feed', 'float', 3.14, sensitive: true); - $status = $config->statusCache(); - $this->assertStringStartsWith(self::invokePrivate(AppConfig::class, 'ENCRYPTION_PREFIX'), $status['fastCache']['feed']['float']); - } - - public function testSetSensitiveValueFloatDatabase(): void { - $config = $this->generateAppConfig(); - $config->setValueFloat('feed', 'float', 3.14, sensitive: true); - $config->clearCache(); - $this->assertSame(3.14, $config->getValueFloat('feed', 'float', 0)); - } - - public function testSetNonSensitiveValueFloatAsSensitive(): void { - $config = $this->generateAppConfig(); - $config->setValueFloat('feed', 'float', 3.14); - $config->setValueFloat('feed', 'float', 3.14, sensitive: true); - $this->assertSame(true, $config->isSensitive('feed', 'float')); - } - - public function testSetSensitiveValueFloatAsNonSensitiveStaysSensitive(): void { - $config = $this->generateAppConfig(); - $config->setValueFloat('feed', 'float', 3.14, sensitive: true); - $config->setValueFloat('feed', 'float', 1.23); - $this->assertSame(true, $config->isSensitive('feed', 'float')); - } - - public function testSetSensitiveValueFloatAsNonSensitiveAreStillUpdated(): void { - $config = $this->generateAppConfig(); - $config->setValueFloat('feed', 'float', 3.14, sensitive: true); - $config->setValueFloat('feed', 'float', 1.23); - $this->assertSame(1.23, $config->getValueFloat('feed', 'float', 0)); - } - - public function testSetLazyValueBool(): void { - $config = $this->generateAppConfig(); - $config->setValueBool('feed', 'bool', true, true); - $this->assertSame(true, $config->getValueBool('feed', 'bool', false, true)); - } - - public function testSetLazyValueBoolCache(): void { - $config = $this->generateAppConfig(); - $config->setValueBool('feed', 'bool', true, true); - $status = $config->statusCache(); - $this->assertSame('1', $status['lazyCache']['feed']['bool']); - } - - public function testSetLazyValueBoolDatabase(): void { - $config = $this->generateAppConfig(); - $config->setValueBool('feed', 'bool', true, true); - $config->clearCache(); - $this->assertSame(true, $config->getValueBool('feed', 'bool', false, true)); - } - - public function testSetLazyValueBoolAsNonLazy(): void { - $config = $this->generateAppConfig(); - $config->setValueBool('feed', 'bool', true, true); - $config->setValueBool('feed', 'bool', true, false); - $this->assertSame(true, $config->getValueBool('feed', 'bool', false)); - } - - public function testSetNonLazyValueBoolAsLazy(): void { - $config = $this->generateAppConfig(); - $config->setValueBool('feed', 'bool', true, false); - $config->setValueBool('feed', 'bool', true, true); - $this->assertSame(true, $config->getValueBool('feed', 'bool', false, true)); - } - - public function testSetLazyValueArray(): void { - $config = $this->generateAppConfig(); - $config->setValueArray('feed', 'array', ['test' => 1], true); - $this->assertSame(['test' => 1], $config->getValueArray('feed', 'array', [], true)); - } - - public function testSetLazyValueArrayCache(): void { - $config = $this->generateAppConfig(); - $config->setValueArray('feed', 'array', ['test' => 1], true); - $status = $config->statusCache(); - $this->assertSame('{"test":1}', $status['lazyCache']['feed']['array']); - } - - public function testSetLazyValueArrayDatabase(): void { - $config = $this->generateAppConfig(); - $config->setValueArray('feed', 'array', ['test' => 1], true); - $config->clearCache(); - $this->assertSame(['test' => 1], $config->getValueArray('feed', 'array', [], true)); - } - - public function testSetLazyValueArrayAsNonLazy(): void { - $config = $this->generateAppConfig(); - $config->setValueArray('feed', 'array', ['test' => 1], true); - $config->setValueArray('feed', 'array', ['test' => 1], false); - $this->assertSame(['test' => 1], $config->getValueArray('feed', 'array', [])); - } - - public function testSetNonLazyValueArrayAsLazy(): void { - $config = $this->generateAppConfig(); - $config->setValueArray('feed', 'array', ['test' => 1], false); - $config->setValueArray('feed', 'array', ['test' => 1], true); - $this->assertSame(['test' => 1], $config->getValueArray('feed', 'array', [], true)); - } - - - public function testSetSensitiveValueArray(): void { - $config = $this->generateAppConfig(); - $config->setValueArray('feed', 'array', ['test' => 1], sensitive: true); - $this->assertEqualsCanonicalizing(['test' => 1], $config->getValueArray('feed', 'array', [])); - } - - public function testSetSensitiveValueArrayCache(): void { - $config = $this->generateAppConfig(); - $config->setValueArray('feed', 'array', ['test' => 1], sensitive: true); - $status = $config->statusCache(); - $this->assertStringStartsWith(self::invokePrivate(AppConfig::class, 'ENCRYPTION_PREFIX'), $status['fastCache']['feed']['array']); - } - - public function testSetSensitiveValueArrayDatabase(): void { - $config = $this->generateAppConfig(); - $config->setValueArray('feed', 'array', ['test' => 1], sensitive: true); - $config->clearCache(); - $this->assertEqualsCanonicalizing(['test' => 1], $config->getValueArray('feed', 'array', [])); - } - - public function testSetNonSensitiveValueArrayAsSensitive(): void { - $config = $this->generateAppConfig(); - $config->setValueArray('feed', 'array', ['test' => 1]); - $config->setValueArray('feed', 'array', ['test' => 1], sensitive: true); - $this->assertSame(true, $config->isSensitive('feed', 'array')); - } - - public function testSetSensitiveValueArrayAsNonSensitiveStaysSensitive(): void { - $config = $this->generateAppConfig(); - $config->setValueArray('feed', 'array', ['test' => 1], sensitive: true); - $config->setValueArray('feed', 'array', ['test' => 2]); - $this->assertSame(true, $config->isSensitive('feed', 'array')); - } - - public function testSetSensitiveValueArrayAsNonSensitiveAreStillUpdated(): void { - $config = $this->generateAppConfig(); - $config->setValueArray('feed', 'array', ['test' => 1], sensitive: true); - $config->setValueArray('feed', 'array', ['test' => 2]); - $this->assertEqualsCanonicalizing(['test' => 2], $config->getValueArray('feed', 'array', [])); - } - - public function testUpdateNotSensitiveToSensitive(): void { - $config = $this->generateAppConfig(); - $config->updateSensitive('non-sensitive-app', 'lazy-key', true); - $this->assertSame(true, $config->isSensitive('non-sensitive-app', 'lazy-key', true)); - } - - public function testUpdateSensitiveToNotSensitive(): void { - $config = $this->generateAppConfig(); - $config->updateSensitive('sensitive-app', 'lazy-key', false); - $this->assertSame(false, $config->isSensitive('sensitive-app', 'lazy-key', true)); - } - - public function testUpdateSensitiveToSensitiveReturnsFalse(): void { - $config = $this->generateAppConfig(); - $this->assertSame(false, $config->updateSensitive('sensitive-app', 'lazy-key', true)); - } - - public function testUpdateNotSensitiveToNotSensitiveReturnsFalse(): void { - $config = $this->generateAppConfig(); - $this->assertSame(false, $config->updateSensitive('non-sensitive-app', 'lazy-key', false)); - } - - public function testUpdateSensitiveOnUnknownKeyReturnsFalse(): void { - $config = $this->generateAppConfig(); - $this->assertSame(false, $config->updateSensitive('non-sensitive-app', 'unknown-key', true)); - } - - public function testUpdateNotLazyToLazy(): void { - $config = $this->generateAppConfig(); - $config->updateLazy('non-sensitive-app', 'non-lazy-key', true); - $this->assertSame(true, $config->isLazy('non-sensitive-app', 'non-lazy-key')); - } - - public function testUpdateLazyToNotLazy(): void { - $config = $this->generateAppConfig(); - $config->updateLazy('non-sensitive-app', 'lazy-key', false); - $this->assertSame(false, $config->isLazy('non-sensitive-app', 'lazy-key')); - } - - public function testUpdateLazyToLazyReturnsFalse(): void { - $config = $this->generateAppConfig(); - $this->assertSame(false, $config->updateLazy('non-sensitive-app', 'lazy-key', true)); - } - - public function testUpdateNotLazyToNotLazyReturnsFalse(): void { - $config = $this->generateAppConfig(); - $this->assertSame(false, $config->updateLazy('non-sensitive-app', 'non-lazy-key', false)); - } - - public function testUpdateLazyOnUnknownKeyReturnsFalse(): void { - $config = $this->generateAppConfig(); - $this->assertSame(false, $config->updateLazy('non-sensitive-app', 'unknown-key', true)); - } - - public function testGetDetails(): void { - $config = $this->generateAppConfig(); - $this->assertEquals( - [ - 'app' => 'non-sensitive-app', - 'key' => 'lazy-key', - 'value' => 'value', - 'type' => 4, - 'lazy' => true, - 'typeString' => 'string', - 'sensitive' => false, - ], - $config->getDetails('non-sensitive-app', 'lazy-key') - ); - } - - public function testGetDetailsSensitive(): void { - $config = $this->generateAppConfig(); - $this->assertEquals( - [ - 'app' => 'sensitive-app', - 'key' => 'lazy-key', - 'value' => 'value', - 'type' => 4, - 'lazy' => true, - 'typeString' => 'string', - 'sensitive' => true, - ], - $config->getDetails('sensitive-app', 'lazy-key') - ); - } - - public function testGetDetailsInt(): void { - $config = $this->generateAppConfig(); - $this->assertEquals( - [ - 'app' => 'typed', - 'key' => 'int', - 'value' => '42', - 'type' => 8, - 'lazy' => false, - 'typeString' => 'integer', - 'sensitive' => false - ], - $config->getDetails('typed', 'int') - ); - } - - public function testGetDetailsFloat(): void { - $config = $this->generateAppConfig(); - $this->assertEquals( - [ - 'app' => 'typed', - 'key' => 'float', - 'value' => '3.14', - 'type' => 16, - 'lazy' => false, - 'typeString' => 'float', - 'sensitive' => false - ], - $config->getDetails('typed', 'float') - ); - } - - public function testGetDetailsBool(): void { - $config = $this->generateAppConfig(); - $this->assertEquals( - [ - 'app' => 'typed', - 'key' => 'bool', - 'value' => '1', - 'type' => 32, - 'lazy' => false, - 'typeString' => 'boolean', - 'sensitive' => false - ], - $config->getDetails('typed', 'bool') - ); - } - - public function testGetDetailsArray(): void { - $config = $this->generateAppConfig(); - $this->assertEquals( - [ - 'app' => 'typed', - 'key' => 'array', - 'value' => '{"test": 1}', - 'type' => 64, - 'lazy' => false, - 'typeString' => 'array', - 'sensitive' => false - ], - $config->getDetails('typed', 'array') - ); - } - - public function testDeleteKey(): void { - $config = $this->generateAppConfig(); - $config->deleteKey('anotherapp', 'key'); - $this->assertSame('default', $config->getValueString('anotherapp', 'key', 'default')); - } - - public function testDeleteKeyCache(): void { - $config = $this->generateAppConfig(); - $config->deleteKey('anotherapp', 'key'); - $status = $config->statusCache(); - $this->assertEqualsCanonicalizing(['enabled' => 'no', 'installed_version' => '3.2.1'], $status['fastCache']['anotherapp']); - } - - public function testDeleteKeyDatabase(): void { - $config = $this->generateAppConfig(); - $config->deleteKey('anotherapp', 'key'); - $config->clearCache(); - $this->assertSame('default', $config->getValueString('anotherapp', 'key', 'default')); - } - - public function testDeleteApp(): void { - $config = $this->generateAppConfig(); - $config->deleteApp('anotherapp'); - $this->assertSame('default', $config->getValueString('anotherapp', 'key', 'default')); - $this->assertSame('default', $config->getValueString('anotherapp', 'enabled', 'default')); - } - - public function testDeleteAppCache(): void { - $config = $this->generateAppConfig(); - $status = $config->statusCache(); - $this->assertSame(true, isset($status['fastCache']['anotherapp'])); - $config->deleteApp('anotherapp'); - $status = $config->statusCache(); - $this->assertSame(false, isset($status['fastCache']['anotherapp'])); - } - - public function testDeleteAppDatabase(): void { - $config = $this->generateAppConfig(); - $config->deleteApp('anotherapp'); - $config->clearCache(); - $this->assertSame('default', $config->getValueString('anotherapp', 'key', 'default')); - $this->assertSame('default', $config->getValueString('anotherapp', 'enabled', 'default')); - } - - public function testClearCache(): void { - $config = $this->generateAppConfig(); - $config->setValueString('feed', 'string', '123454'); - $config->clearCache(); - $status = $config->statusCache(); - $this->assertSame([], $status['fastCache']); - } - - public function testSensitiveValuesAreEncrypted(): void { - $key = self::getUniqueID('secret'); - - $appConfig = $this->generateAppConfig(); - $secret = md5((string)time()); - $appConfig->setValueString('testapp', $key, $secret, sensitive: true); - - $this->assertConfigValueNotEquals('testapp', $key, $secret); - - // Can get in same run - $actualSecret = $appConfig->getValueString('testapp', $key); - $this->assertEquals($secret, $actualSecret); - - // Can get freshly decrypted from DB - $newAppConfig = $this->generateAppConfig(); - $actualSecret = $newAppConfig->getValueString('testapp', $key); - $this->assertEquals($secret, $actualSecret); - } - - public function testMigratingNonSensitiveValueToSensitiveWithSetValue(): void { - $key = self::getUniqueID('secret'); - $appConfig = $this->generateAppConfig(); - $secret = sha1((string)time()); - - // Unencrypted - $appConfig->setValueString('testapp', $key, $secret); - $this->assertConfigKey('testapp', $key, $secret); - - // Can get freshly decrypted from DB - $newAppConfig = $this->generateAppConfig(); - $actualSecret = $newAppConfig->getValueString('testapp', $key); - $this->assertEquals($secret, $actualSecret); - - // Encrypting on change - $appConfig->setValueString('testapp', $key, $secret, sensitive: true); - $this->assertConfigValueNotEquals('testapp', $key, $secret); - - // Can get in same run - $actualSecret = $appConfig->getValueString('testapp', $key); - $this->assertEquals($secret, $actualSecret); - - // Can get freshly decrypted from DB - $newAppConfig = $this->generateAppConfig(); - $actualSecret = $newAppConfig->getValueString('testapp', $key); - $this->assertEquals($secret, $actualSecret); - } - - public function testUpdateSensitiveValueToNonSensitiveWithUpdateSensitive(): void { - $key = self::getUniqueID('secret'); - $appConfig = $this->generateAppConfig(); - $secret = sha1((string)time()); - - // Encrypted - $appConfig->setValueString('testapp', $key, $secret, sensitive: true); - $this->assertConfigValueNotEquals('testapp', $key, $secret); - - // Migrate to non-sensitive / non-encrypted - $appConfig->updateSensitive('testapp', $key, false); - $this->assertConfigKey('testapp', $key, $secret); - } - - public function testUpdateNonSensitiveValueToSensitiveWithUpdateSensitive(): void { - $key = self::getUniqueID('secret'); - $appConfig = $this->generateAppConfig(); - $secret = sha1((string)time()); - - // Unencrypted - $appConfig->setValueString('testapp', $key, $secret); - $this->assertConfigKey('testapp', $key, $secret); - - // Migrate to sensitive / encrypted - $appConfig->updateSensitive('testapp', $key, true); - $this->assertConfigValueNotEquals('testapp', $key, $secret); - } - - public function testSearchKeyNoLazyLoading(): void { - $appConfig = $this->generateAppConfig(); - $appConfig->searchKeys('searchtest', 'search_'); - $status = $appConfig->statusCache(); - $this->assertFalse($status['lazyLoaded'], 'searchKeys() loaded lazy config'); - } - - public function testSearchKeyFast(): void { - $appConfig = $this->generateAppConfig(); - $this->assertEquals(['search_key1', 'search_key2', 'search_key3'], $appConfig->searchKeys('searchtest', 'search_')); - } - - public function testSearchKeyLazy(): void { - $appConfig = $this->generateAppConfig(); - $this->assertEquals(['search_key5_lazy'], $appConfig->searchKeys('searchtest', 'search_', true)); - } - - protected function loadConfigValueFromDatabase(string $app, string $key): string|false { - $sql = $this->connection->getQueryBuilder(); - $sql->select('configvalue') - ->from('appconfig') - ->where($sql->expr()->eq('appid', $sql->createParameter('appid'))) - ->andWhere($sql->expr()->eq('configkey', $sql->createParameter('configkey'))) - ->setParameter('appid', $app) - ->setParameter('configkey', $key); - $query = $sql->executeQuery(); - $actual = $query->fetchOne(); - $query->closeCursor(); - - return $actual; - } - - protected function assertConfigKey(string $app, string $key, string|false $expected): void { - $this->assertEquals($expected, $this->loadConfigValueFromDatabase($app, $key)); } - protected function assertConfigValueNotEquals(string $app, string $key, string|false $expected): void { - $this->assertNotEquals($expected, $this->loadConfigValueFromDatabase($app, $key)); + public function testCachedRead(): void { + $this->localCache->expects(self::once()) + ->method('get') + ->with('OC\\AppConfig') + ->willReturn([ + 'fastCache' => [ + 'appid' => [ + 'some-key' => 'some-value', + 'other-key' => 'other value' + ], + ], + 'valueTypes' => [ + 'appid' => [ + 'some-key' => AppConfig::VALUE_STRING, + 'other-key' => AppConfig::VALUE_STRING, + ], + ], + ]); + + $this->connection->expects(self::never())->method('getQueryBuilder'); + $config = $this->getAppConfig(true); + + + $this->assertSame('some-value', $config->getValueString('appid', 'some-key')); + $this->assertSame('other value', $config->getValueString('appid', 'other-key')); + $this->assertSame(AppConfig::VALUE_STRING, $config->getValueType('appid', 'some-key', false)); + } + + public function testCachedLazyRead(): void { + $this->localCache->expects(self::once()) + ->method('get') + ->with('OC\\AppConfig') + ->willReturn([ + 'fastCache' => [ + 'appid' => [ + 'fast-key' => 'fast value', + ], + ], + 'lazyCache' => [ + 'appid' => [ + 'lazy-key' => 'lazy value', + ], + ], + 'valueTypes' => [ + 'appid' => [ + 'some-key' => AppConfig::VALUE_STRING, + 'lazy-key' => AppConfig::VALUE_STRING, + ], + ], + ]); + + $this->connection->expects(self::never())->method('getQueryBuilder'); + $config = $this->getAppConfig(true); + + + $this->assertSame('fast value', $config->getValueString('appid', 'fast-key')); + $this->assertSame('lazy value', $config->getValueString('appid', 'lazy-key', '', true)); + } + + public function testOnlyFastKeyCached(): void { + $this->localCache->expects(self::atLeastOnce()) + ->method('get') + ->with('OC\\AppConfig') + ->willReturn([ + 'fastCache' => [ + 'appid' => [ + 'fast-key' => 'fast value', + ], + ], + 'valueTypes' => [ + 'appid' => [ + 'fast-key' => AppConfig::VALUE_STRING, + ], + ], + ]); + + $result = $this->createMock(IResult::class); + $result->method('fetchAll')->willReturn([ + ['lazy' => 1, 'appid' => 'appid', 'configkey' => 'lazy-key', 'configvalue' => 'lazy value'], + ]); + $expression = $this->createMock(IExpressionBuilder::class); + $queryBuilder = $this->createMock(IQueryBuilder::class); + $queryBuilder->method('from')->willReturn($queryBuilder); + $queryBuilder->method('expr')->willReturn($expression); + $queryBuilder->method('executeQuery')->willReturn($result); + + $this->connection->expects(self::once())->method('getQueryBuilder')->willReturn($queryBuilder); + $config = $this->getAppConfig(true); + + + $this->assertSame('fast value', $config->getValueString('appid', 'fast-key')); + $this->assertSame('lazy value', $config->getValueString('appid', 'lazy-key', '', true)); + } + + public function testWritesAreCached(): void { + $this->localCache->expects(self::atLeastOnce()) + ->method('get') + ->with('OC\\AppConfig') + ->willReturn([ + 'fastCache' => [ + 'appid' => [ + 'first-key' => 'first value', + ], + ], + 'valueTypes' => [ + 'appid' => [ + 'first-key' => AppConfig::VALUE_STRING, + ], + ], + ]); + + $expression = $this->createMock(IExpressionBuilder::class); + $queryBuilder = $this->createMock(IQueryBuilder::class); + $queryBuilder->expects(self::once()) + ->method('update') + ->with('appconfig', null) + ->willReturn($queryBuilder); + $queryBuilder->method('set')->willReturn($queryBuilder); + $queryBuilder->method('where')->willReturn($queryBuilder); + $queryBuilder->method('andWhere')->willReturn($queryBuilder); + $queryBuilder->method('expr')->willReturn($expression); + $this->connection->expects(self::once())->method('getQueryBuilder')->willReturn($queryBuilder); + + $config = $this->getAppConfig(true); + + $this->assertSame('first value', $config->getValueString('appid', 'first-key')); + $config->setValueString('appid', 'first-key', 'new value'); + $this->assertSame('new value', $config->getValueString('appid', 'first-key')); } } diff --git a/tests/lib/Memcache/FactoryTest.php b/tests/lib/Memcache/FactoryTest.php index e16e079349f35..31500f31b65d5 100644 --- a/tests/lib/Memcache/FactoryTest.php +++ b/tests/lib/Memcache/FactoryTest.php @@ -12,6 +12,7 @@ use OC\Memcache\NullCache; use OCP\HintException; use OCP\Profiler\IProfiler; +use OCP\ServerVersion; use Psr\Log\LoggerInterface; class Test_Factory_Available_Cache1 extends NullCache { @@ -111,7 +112,8 @@ public function testCacheAvailability($localCache, $distributedCache, $lockingCa $expectedLocalCache, $expectedDistributedCache, $expectedLockingCache): void { $logger = $this->getMockBuilder(LoggerInterface::class)->getMock(); $profiler = $this->getMockBuilder(IProfiler::class)->getMock(); - $factory = new Factory(fn () => 'abc', $logger, $profiler, $localCache, $distributedCache, $lockingCache); + $serverVersion = $this->createMock(ServerVersion::class); + $factory = new Factory($logger, $profiler, $serverVersion, $localCache, $distributedCache, $lockingCache); $this->assertTrue(is_a($factory->createLocal(), $expectedLocalCache)); $this->assertTrue(is_a($factory->createDistributed(), $expectedDistributedCache)); $this->assertTrue(is_a($factory->createLocking(), $expectedLockingCache)); @@ -123,13 +125,15 @@ public function testCacheNotAvailableException($localCache, $distributedCache): $logger = $this->getMockBuilder(LoggerInterface::class)->getMock(); $profiler = $this->getMockBuilder(IProfiler::class)->getMock(); - new Factory(fn () => 'abc', $logger, $profiler, $localCache, $distributedCache); + $serverVersion = $this->createMock(ServerVersion::class); + new Factory($logger, $profiler, $serverVersion, $localCache, $distributedCache); } public function testCreateInMemory(): void { $logger = $this->getMockBuilder(LoggerInterface::class)->getMock(); $profiler = $this->getMockBuilder(IProfiler::class)->getMock(); - $factory = new Factory(fn () => 'abc', $logger, $profiler, null, null, null); + $serverVersion = $this->createMock(ServerVersion::class); + $factory = new Factory($logger, $profiler, $serverVersion, null, null, null); $cache = $factory->createInMemory(); $cache->set('test', 48);