Improve settings handling and add tests (#6270)

* Fix #6266: BaseSettingsManager::deleteAll() does use prefix as wildcard

* Enh #6271: Add input and type checks, as well as strict types to SettingsManager

* Fix #6267 SettingsManager::flushContentContainer() only clears the collection in the current instance, not the underlying cache

* Enh #6272: Always return integer from settings, if value can be converted

* Improve \humhub\libs\BaseSettingsManager::getSerialized() to allow return value be an object and throw an exception on decoding error

* Enh #6270: Add tests for SettingsManager
This commit is contained in:
Martin Rüegg 2023-05-03 11:55:00 +02:00 committed by GitHub
parent 184c33ad46
commit 1b0700deed
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 1273 additions and 166 deletions

View File

@ -3,6 +3,11 @@ HumHub Changelog
1.15.0 (Unreleased)
-------------------
- Enh #6270: Add tests for SettingsManager
- Enh #6272: Always return integer from settings, if value can be converted
- Fix #6267: SettingsManager::flushContentContainer() only clears the collection in the current instance, not the underlying cache
- Enh #6271: Add input and type checks, as well as strict types to SettingsManager
- Fix #6266: BaseSettingsManager::deleteAll() does use prefix as wildcard
- Fix #6259: Add json & pdo extensions as requirement; updating composer dependencies and node modules
- Fix #6192: Where Group::getAdminGroupId() would sometimes return int, sometimes string
- Enh #6260: Improve migration class

18
MIGRATE-DEV.md Normal file
View File

@ -0,0 +1,18 @@
Module Migration Guide
======================
See [humhub/documentation::docs/develop/modules-migrate.md](https://github.com/humhub/documentation/blob/master/docs/develop/modules-migrate.md)
for full version.
Version 1.15 (Unreleased)
-------------------------
### Behaviour change
- `\humhub\libs\BaseSettingsManager::deleteAll()` no longer uses the `$prefix` parameter as a full wildcard, but
actually as a prefix. Use `$prefix = '%pattern%'` to get the old behaviour. Or use `$parameter = '%suffix'` if you
want to match against the end of the names.
- `\humhub\libs\BaseSettingsManager::get()` now returns a pure int in case the (trimmed) value can be converted
### Type restrictions
- `\humhub\libs\BaseSettingsManager` and its child classes on fields, method parameters, & return types

View File

@ -9,6 +9,7 @@
namespace humhub\components;
use Yii;
use yii\base\InvalidCallException;
use yii\db\ActiveRecord;
/**
@ -22,24 +23,38 @@ abstract class SettingActiveRecord extends ActiveRecord
/**
* @const array List of fields to be used to generate the cache key
*/
protected const CACHE_KEY_FIELDS = ['module_id'];
public const CACHE_KEY_FIELDS = ['module_id'];
/**
* @const string Used as the formatting pattern for sprintf when generating the cache key
*/
protected const CACHE_KEY_FORMAT = 'settings-%s';
public const CACHE_KEY_FORMAT = 'settings-%s';
/**
* @param string|array|null $condition
* @param array $params
*
* @return int
* @noinspection PhpMissingReturnTypeInspection
*/
public static function deleteAll($condition = null, $params = [])
{
if (static::class === self::class) {
throw new InvalidCallException(sprintf(
'Method %s may not be called from the abstract class, but MUST be called from the implementing class, as otherwise tablename() is not returning a correct table.',
__METHOD__
));
}
// get a grouped list of cache entries that are going to be deleted, grouped by static::CACHE_KEY_FIELDS
$containers = self::find()
$modulesOrContainers = self::find()
->where($condition, $params)
->groupBy(static::CACHE_KEY_FIELDS)
->select(static::CACHE_KEY_FIELDS)
->all();
// going through that list, deleting the respective cache
array_walk($containers, static function (ActiveRecord $rec) {
array_walk($modulesOrContainers, static function (ActiveRecord $rec) {
$key = static::getCacheKey(...array_values($rec->toArray()));
Yii::$app->cache->delete($key);
});
@ -49,8 +64,8 @@ abstract class SettingActiveRecord extends ActiveRecord
}
/**
* @param string $moduleId Name of the module to create the cache key for
* @param mixed ...$values Additional arguments, if required by the static::CACHE_KEY_FORMAT
* @param string $moduleId Name of the module to create the cache key for
* @param mixed ...$values Additional arguments, if required by the static::CACHE_KEY_FORMAT
*
* @return string The key used for cache operation
*/

View File

@ -8,6 +8,10 @@
namespace humhub\components;
use humhub\modules\content\components\ContentContainerController;
use humhub\modules\space\models\Space;
use humhub\modules\user\models\User;
use Throwable;
use Yii;
use humhub\libs\BaseSettingsManager;
use humhub\modules\content\components\ContentContainerActiveRecord;
@ -21,11 +25,10 @@ use humhub\modules\content\components\ContentContainerSettingsManager;
*/
class SettingsManager extends BaseSettingsManager
{
/**
* @var ContentContainerSettingsManager[] already loaded content container settings managers
*/
protected $contentContainers = [];
protected array $contentContainers = [];
/**
* Returns content container
@ -33,18 +36,16 @@ class SettingsManager extends BaseSettingsManager
* @param ContentContainerActiveRecord $container
* @return ContentContainerSettingsManager
*/
public function contentContainer(ContentContainerActiveRecord $container)
public function contentContainer(ContentContainerActiveRecord $container): ContentContainerSettingsManager
{
if (isset($this->contentContainers[$container->contentcontainer_id])) {
return $this->contentContainers[$container->contentcontainer_id];
if ($contentContainers = $this->contentContainers[$container->contentcontainer_id] ?? null) {
return $contentContainers;
}
$this->contentContainers[$container->contentcontainer_id] = new ContentContainerSettingsManager([
return $this->contentContainers[$container->contentcontainer_id] = new ContentContainerSettingsManager([
'moduleId' => $this->moduleId,
'contentContainer' => $container,
]);
return $this->contentContainers[$container->contentcontainer_id];
}
@ -52,24 +53,38 @@ class SettingsManager extends BaseSettingsManager
* Clears runtime cached content container settings
*
* @param ContentContainerActiveRecord|null $container if null all content containers will be flushed
*
* @noinspection PhpUnused
*/
public function flushContentContainer(ContentContainerActiveRecord $container = null)
{
if ($container === null) {
$containers = $this->contentContainers;
$this->contentContainers = [];
} else {
// need to create an instance, if it does not already exist, in order to then flush the underlying cache
$containers = [$this->contentContainer($container)] ?? null;
unset($this->contentContainers[$container->contentcontainer_id]);
}
array_walk($containers, static fn(ContentContainerSettingsManager $container) => $container->invalidateCache());
}
/**
* Returns ContentContainerSettingsManager for the given $user or current logged in user
* @return ContentContainerSettingsManager
* Returns ContentContainerSettingsManager for the given $user or current logged-in user
*
* @param User|null $user
*
* @return ContentContainerSettingsManager|null
* @throws Throwable
*/
public function user($user = null)
public function user(?User $user = null): ?ContentContainerSettingsManager
{
if (!$user) {
if ($user === null) {
$user = Yii::$app->user->getIdentity();
if (!$user instanceof User) {
return null;
}
}
return $this->contentContainer($user);
@ -77,27 +92,37 @@ class SettingsManager extends BaseSettingsManager
/**
* Returns ContentContainerSettingsManager for the given $space or current controller space
*
* @param Space|null $space
*
* @return ContentContainerSettingsManager
*/
public function space($space = null)
public function space(?Space $space = null): ?ContentContainerSettingsManager
{
if ($space != null) {
if ($space !== null) {
return $this->contentContainer($space);
} elseif (Yii::$app->controller instanceof \humhub\modules\content\components\ContentContainerController) {
if (Yii::$app->controller->contentContainer instanceof \humhub\modules\space\models\Space) {
return $this->contentContainer(Yii::$app->controller->contentContainer);
}
}
/** @noinspection PhpPossiblePolymorphicInvocationInspection */
if (
($controller = Yii::$app->controller) instanceof ContentContainerController
&& ($space = $controller->contentContainer) instanceof Space
) {
return $this->contentContainer($space);
}
return null;
}
/**
* Indicates this setting is fixed in configuration file and cannot be
* changed at runtime.
*
* @param string $name
* @param string|int $name
*
* @return boolean
*/
public function isFixed($name)
public function isFixed(string $name): bool
{
return isset(Yii::$app->params['fixed-settings'][$this->moduleId][$name]);
}

View File

@ -523,4 +523,4 @@ return [
'defer',
],
],
];
];

View File

@ -0,0 +1,35 @@
<?php
/**
* @link https://www.humhub.org/
* @copyright Copyright (c) 2023 HumHub GmbH & Co. KG
* @license https://www.humhub.com/licences
*/
namespace humhub\exceptions;
use yii\base\InvalidArgumentException;
/**
* @since 1.15
*/
class InvalidArgumentTypeException extends InvalidArgumentException
{
use InvalidTypeExceptionTrait;
protected function formatPrologue(array $constructArguments): string
{
$argumentName = is_array($this->parameter)
? reset($this->parameter)
: null;
$argumentNumber = is_array($this->parameter)
? key($this->parameter)
: $this->parameter;
$argumentName = $argumentName === null
? ''
: " \$" . ltrim($argumentName, '$');
return sprintf('Argument #%d%s', $argumentNumber, $argumentName);
}
}

View File

@ -0,0 +1,24 @@
<?php
/**
* @link https://www.humhub.org/
* @copyright Copyright (c) 2023 HumHub GmbH & Co. KG
* @license https://www.humhub.com/licences
*/
namespace humhub\exceptions;
use yii\base\InvalidConfigException;
/**
* @since 1.15
*/
class InvalidConfigTypeException extends InvalidConfigException
{
use InvalidTypeExceptionTrait;
protected function formatPrologue(array $constructArguments): string
{
return "Parameter $this->parameter of configuration";
}
}

View File

@ -0,0 +1,75 @@
<?php
/**
* @link https://www.humhub.org/
* @copyright Copyright (c) 2023 HumHub GmbH & Co. KG
* @license https://www.humhub.com/licences
*/
namespace humhub\exceptions;
/**
* @since 1.15
*/
trait InvalidTypeExceptionTrait
{
// public properties
public string $methodName;
public $parameter;
public array $validType = [];
/**
* @var mixed|null
*/
public $givenValue;
/**
* @param string $method
* @param int|array $parameter = [
* int => string, // position, or [ position => name ] of the argument
* ]
* @param array|string|null $validType
* @param null $givenValue
*/
public function __construct(
$method = '',
$parameter = null,
$validType = [],
$givenValue = null,
$nullable = false,
$code = 0,
$previous = null
) {
$this->methodName = $method;
$this->parameter = $parameter;
$this->validType = (array)($validType ?? ['mixed']);
$this->givenValue = $givenValue;
if ($nullable && !in_array('null', $this->validType, true)) {
$this->validType[] = 'null';
}
$message = sprintf(
'%s passed to %s must be of type %s, %s given.',
$this->formatPrologue(func_get_args()),
$this->methodName,
implode(', ', $this->validType),
get_debug_type($this->givenValue)
);
parent::__construct($message, $code, $previous);
}
abstract protected function formatPrologue(array $constructArguments): string;
public function getName(): string
{
if (method_exists(parent::class, 'getName')) {
return parent::getName() . " Type";
}
return 'Invalid Type';
}
}

View File

@ -9,10 +9,12 @@
namespace humhub\libs;
use humhub\components\SettingActiveRecord;
use humhub\exceptions\InvalidArgumentTypeException;
use Stringable;
use Yii;
use yii\base\Component;
use yii\base\Exception;
use yii\base\InvalidArgumentException;
use yii\base\InvalidConfigException;
use yii\db\conditions\LikeCondition;
use yii\db\StaleObjectException;
use yii\helpers\Json;
@ -26,9 +28,9 @@ use yii\helpers\Json;
abstract class BaseSettingsManager extends Component
{
/**
* @var string|null module id this settings manager belongs to.
* @var string module id this settings manager belongs to.
*/
public ?string $moduleId = null;
public string $moduleId;
/**
* @var array|null of loaded settings
@ -45,15 +47,20 @@ abstract class BaseSettingsManager extends Component
*/
public function init()
{
if ($this->moduleId === null) {
throw new Exception('Could not determine module id');
try {
if ($this->moduleId === '') {
throw new InvalidConfigException('Empty module id!', 1);
}
} catch (InvalidConfigException $t) {
throw $t;
} catch (\Throwable $t) {
throw new InvalidConfigException('Module id not set!', 2);
}
if (static::isDatabaseInstalled()) {
$this->loadValues();
}
parent::init();
}
@ -61,12 +68,21 @@ abstract class BaseSettingsManager extends Component
* Sets a settings value
*
* @param string $name
* @param string $value
* @param string|int|bool $value
*
* @return void
*/
public function set($name, $value)
public function set(string $name, $value)
{
if ($name === '') {
throw new InvalidArgumentException(
sprintf('Argument #1 ($name) passed to %s may not be an empty string!', __METHOD__)
);
}
if ($value === null) {
return $this->delete($name);
$this->delete($name);
return;
}
// Update database setting record
@ -107,15 +123,23 @@ abstract class BaseSettingsManager extends Component
*
* @param string $name
* @param mixed $default the setting value or null when not exists
* @param bool $asArray whether to return objects in terms of associative arrays.
* @param bool $throwException if true then throw an exception upon error, rather than returning the serialized string
*
* @return mixed|string|null
*/
public function getSerialized(string $name, $default = null)
public function getSerialized(string $name, $default = null, bool $asArray = true, bool $throwException = false)
{
$value = $this->get($name, $default);
if (is_string($value)) {
try {
$value = Json::decode($value);
$value = Json::decode($value, $asArray);
} catch (InvalidArgumentException $ex) {
Yii::error($ex->getMessage());
if ($throwException) {
throw $ex;
}
}
}
return $value;
@ -124,13 +148,16 @@ abstract class BaseSettingsManager extends Component
/**
* Returns value of setting
*
* @param string $name the name of setting
* @param string|int $name the name of setting
*
* @return string|null the setting value or null when not exists
* @return string|mixed|null the setting value or null when not exists
*/
public function get(string $name, $default = null)
{
return $this->_loaded[$name] ?? $default;
$value = $this->_loaded[$name] ?? null;
// make sure it is an int, if it is possible
return filter_var($value, FILTER_VALIDATE_INT, FILTER_NULL_ON_FAILURE) ?? $value ?? $default;
}
/**
@ -243,18 +270,34 @@ abstract class BaseSettingsManager extends Component
/**
* Deletes all stored settings
*
* @param string|null $prefix if set only delete settings with given name prefix (e.g. theme.)
* @param string|array|Stringable|null $prefix if set, only delete settings with given name prefix (e.g. "theme.")
* Versions before 1.15 used the `$prefix` parameter as a full wildcard (`'%pattern%'`) and not actually as a prefix. Use
* `$prefix = '%pattern%'` to get the old behaviour. Or use `$parameter = '%suffix'` if you want to match
* against the end of the names.
*/
public function deleteAll($prefix = null)
{
$query = $this->find();
if ($prefix !== null) {
$query->andWhere(new LikeCondition('name', 'LIKE', $prefix));
if (StringHelper::isStringable($prefix)) {
if (false === strpos($prefix, "%")) {
$prefix .= "%";
}
} elseif (!is_array($prefix)) {
throw new InvalidArgumentTypeException(
__METHOD__,
[1 => '$prefix'],
['string', 'int', 'null', \Stringable::class],
$prefix
);
}
$query->andWhere(['LIKE', 'name', $prefix, false]);
}
foreach ($query->all() as $setting) {
$this->delete($setting->name);
}
$settings = $query->all();
array_walk($settings, static fn($setting, $i, $self) => $self->delete($setting->name), $this);
}
/**

View File

@ -8,6 +8,8 @@
namespace humhub\libs;
use Stringable;
/**
* StringHelper
*
@ -16,11 +18,11 @@ namespace humhub\libs;
*/
class StringHelper extends \yii\helpers\StringHelper
{
/**
* Converts (LDAP) Binary to Ascii GUID
*
* @param string $object_guid a binary string containing data.
*
* @return string the guid
*/
public static function binaryToGuid($object_guid)
@ -48,4 +50,62 @@ class StringHelper extends \yii\helpers\StringHelper
return strtolower($hex_guid_to_guid_str);
}
/**
* @param mixed $string String to test, and if $convert is true, to turn into string
* @param bool $convert
*
* @return bool
* @since 1.15
*/
public static function isStringable(&$string, bool $convert = true): bool
{
switch (getType($string)) {
case "string":
return true;
case "bool":
case "boolean":
if (!$convert) {
return true;
}
$string = (string)(int)$string;
return true;
case "int":
case "integer":
case "null":
if (!$convert) {
return true;
}
$string = (string)$string;
return true;
case "double":
case "float":
if (!$convert) {
return true;
}
$string = \yii\helpers\StringHelper::floatToString($string);
return true;
case "array":
return false;
case "object":
if ($string instanceof Stringable || (is_object($string) && is_callable([$string, '__toString']))) {
if (!$convert) {
return true;
}
$string = (string)$string;
return true;
}
return false;
default:
// "resource", "NULL", "unknown type", "resource (closed)"
return false;
}
}
}

View File

@ -132,31 +132,27 @@ class Setting extends SettingActiveRecord
*/
public static function fixModuleIdAndName($name, $moduleId)
{
if ($name == 'allowGuestAccess' && $moduleId == 'authentication_internal') {
return ['allowGuestAccess', 'user'];
} elseif ($name == 'defaultUserGroup' && $moduleId == 'authentication_internal') {
return ['auth.allowGuestAccess', 'user'];
} elseif ($name == 'systemEmailAddress' && $moduleId == 'mailing') {
return ['mailer.systemEmailAddress', 'user'];
} elseif ($name == 'systemEmailName' && $moduleId == 'mailing') {
return ['mailer.systemEmailName', 'user'];
} elseif ($name == 'systemEmailReplyTo' && $moduleId == 'mailing') {
return ['mailer.systemEmailReplyTo', 'user'];
} elseif ($name == 'enabled' && $moduleId == 'proxy') {
return ['proxy.enabled', 'base'];
} elseif ($name == 'server' && $moduleId == 'proxy') {
return ['proxy.server', 'base'];
} elseif ($name == 'port' && $moduleId == 'proxy') {
return ['proxy.port', 'base'];
} elseif ($name == 'user' && $moduleId == 'proxy') {
return ['proxy.user', 'base'];
} elseif ($name == 'pass' && $moduleId == 'proxy') {
return ['proxy.password', 'base'];
} elseif ($name == 'noproxy' && $moduleId == 'proxy') {
return ['proxy.noproxy', 'base'];
}
static $translation = [
'authentication_internal' => [
'allowGuestAccess' => ['allowGuestAccess', 'user'],
'defaultUserGroup' => ['auth.allowGuestAccess', 'user'],
],
'mailing' => [
'systemEmailAddress' => ['mailer.systemEmailAddress', 'user'],
'mailing' => ['mailer.systemEmailName', 'user'],
'systemEmailReplyTo' => ['mailer.systemEmailReplyTo', 'user'],
],
'proxy' => [
'enabled' => ['proxy.enabled', 'base'],
'server' => ['proxy.server', 'base'],
'port' => ['proxy.port', 'base'],
'user' => ['proxy.user', 'base'],
'pass' => ['proxy.password', 'base'],
'noproxy' => ['proxy.noproxy', 'base']
]
];
return [$name, $moduleId];
return $translation[$moduleId][$name] ?? [$name, $moduleId];
}
/**

View File

@ -25,10 +25,10 @@ class ContentContainerSetting extends SettingActiveRecord
{
/** @inheritdoc */
protected const CACHE_KEY_FORMAT = 'settings-%s-%d';
public const CACHE_KEY_FORMAT = 'settings-%s-%d';
/** @inheritdoc */
protected const CACHE_KEY_FIELDS = ['module_id', 'contentcontainer_id'];
public const CACHE_KEY_FIELDS = ['module_id', 'contentcontainer_id'];
/**
* @inheritdoc

View File

@ -2,7 +2,7 @@
/**
* HumHub
* Copyright © 2014 The HumHub Project
* Copyright © 2014-2023 The HumHub Project
*
* The texts of the GNU Affero General Public License with an additional
* permission and of our proprietary license can be found at and
@ -17,4 +17,10 @@
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*/
return [];
return [
['name' => 'testSetting1', 'value' => 'Test Setting 1 for User Admin', 'module_id' => 'user', 'contentcontainer_id' => 1 ],
['name' => 'testSetting2', 'value' => 'Test Setting 2 for User Admin', 'module_id' => 'user', 'contentcontainer_id' => 1 ],
['name' => 'testSetting1', 'value' => 'Test Setting 1 for User User1', 'module_id' => 'user', 'contentcontainer_id' => 2 ],
['name' => 'testSetting2', 'value' => 'Test Setting 2 for User User1', 'module_id' => 'user', 'contentcontainer_id' => 2 ],
];

View File

@ -23,4 +23,4 @@ modules:
restart: true
capabilities:
chromeOptions:
args: ["--lang=en-US"]
args: ["--lang=en-US"]

View File

@ -1,11 +1,15 @@
<?php
//Initialize Yii
use Codeception\Configuration;
use Codeception\Util\Autoload;
defined('YII_DEBUG') or define('YII_DEBUG', true);
defined('YII_ENV') or define('YII_ENV', 'test');
defined('YII_ENV_TEST') or define('YII_ENV_TEST', true);
defined('YII_TEST_ENTRY_URL') or define('YII_TEST_ENTRY_URL', parse_url(\Codeception\Configuration::config()['config']['test_entry_url'], PHP_URL_PATH));
defined('YII_TEST_ENTRY_FILE') or define('YII_TEST_ENTRY_FILE', dirname(dirname(dirname(dirname(__DIR__)))) . '/index-test.php');
defined('YII_TEST_ENTRY_URL') or define('YII_TEST_ENTRY_URL', parse_url(Configuration::config()['config']['test_entry_url'], PHP_URL_PATH));
defined('YII_TEST_ENTRY_FILE') or define('YII_TEST_ENTRY_FILE', dirname(__DIR__, 4) . '/index-test.php');
require_once(__DIR__ . '/../../../vendor/autoload.php');
require_once(__DIR__ . '/../../../vendor/yiisoft/yii2/Yii.php');
@ -13,18 +17,18 @@ require_once(__DIR__ . '/../../../vendor/yiisoft/yii2/Yii.php');
$_SERVER['SCRIPT_FILENAME'] = YII_TEST_ENTRY_FILE;
$_SERVER['SCRIPT_NAME'] = YII_TEST_ENTRY_URL;
$_SERVER['SERVER_NAME'] = parse_url(\Codeception\Configuration::config()['config']['test_entry_url'], PHP_URL_HOST);
$_SERVER['SERVER_PORT'] = parse_url(\Codeception\Configuration::config()['config']['test_entry_url'], PHP_URL_PORT) ? : '80';
$_SERVER['SERVER_NAME'] = parse_url(Configuration::config()['config']['test_entry_url'], PHP_URL_HOST);
$_SERVER['SERVER_PORT'] = parse_url(Configuration::config()['config']['test_entry_url'], PHP_URL_PORT) ? : '80';
// Set alias
$config = \Codeception\Configuration::config();
$config = Configuration::config();
$config['test_root'] = isset($config['test_root']) ? $config['test_root'] : dirname(__DIR__);
$config['humhub_root'] = isset($config['humhub_root']) ? $config['humhub_root'] : realpath(dirname(__DIR__ ). '/../../../');
$config['test_root'] = $config['test_root'] ?? dirname(__DIR__);
$config['humhub_root'] = $config['humhub_root'] ?? dirname(__DIR__, 4) . '/';
Yii::setAlias('@tests', $config['test_root']);
Yii::setAlias('@env', '@tests/config/env');
Yii::setAlias('@modules', dirname(dirname(__DIR__)).'/modules');
Yii::setAlias('@modules', dirname(__DIR__, 2) . '/modules');
Yii::setAlias('@root', $config['humhub_root']);
Yii::setAlias('@humhubTests', $config['humhub_root'] . '/protected/humhub/tests');
Yii::setAlias('@humhub', $config['humhub_root'] . '/protected/humhub');
@ -32,8 +36,8 @@ Yii::setAlias('@humhub', $config['humhub_root'] . '/protected/humhub');
Yii::setAlias('@web-static', '/static');
Yii::setAlias('@webroot-static', '@root/static');
// Load all supporting test classes needed for test execution
\Codeception\Util\Autoload::addNamespace('', Yii::getAlias('@humhubTests/codeception/_support'));
\Codeception\Util\Autoload::addNamespace('', Yii::getAlias('@tests/codeception/fixtures'));
\Codeception\Util\Autoload::addNamespace('', Yii::getAlias('@humhubTests/codeception/fixtures'));
\Codeception\Util\Autoload::addNamespace('', Yii::getAlias('@humhubTests/codeception/_pages'));
// Load all supporting test classes needed for test execution
Autoload::addNamespace('', Yii::getAlias('@humhubTests/codeception/_support'));
Autoload::addNamespace('', Yii::getAlias('@tests/codeception/fixtures'));
Autoload::addNamespace('', Yii::getAlias('@humhubTests/codeception/fixtures'));
Autoload::addNamespace('', Yii::getAlias('@humhubTests/codeception/_pages'));

View File

@ -1,53 +1,64 @@
<?php
/*
* @link https://www.humhub.org/
* @copyright Copyright (c) 2023 HumHub GmbH & Co. KG
* @license https://www.humhub.com/licences
*/
/**
* This file is executed before the _bootstrap file and loads the humhub test config
* test.php by means of the env settings.
*/
$codeceptConfig = \Codeception\Configuration::config();
use Codeception\Configuration;
$codeceptConfig = Configuration::config();
$testRoot = $codeceptConfig['test_root'];
$humhubRoot = $codeceptConfig['humhub_root'];
// Parse the environment arguments
$env = isset($GLOBALS['env']) ? $GLOBALS['env'] : [];
$env = $GLOBALS['env'] ?? [];
// If an environment was set try loading special environment config else load default config
if(count($env) > 0) {
\Codeception\Configuration::append(['environment' => $env]);
if (count($env) > 0) {
Configuration::append(['environment' => $env]);
print_r('Run execution environment: '.$env[0]);
$envCfgFile = $testRoot.'/config/env/'.$env[0].'/test.php';
/** @noinspection ForgottenDebugOutputInspection */
print_r('Run execution environment: ' . $env[0]);
$envCfgFile = $testRoot . '/config/env/' . $env[0] . '/test.php';
if (file_exists($envCfgFile)) {
$cfg = array_merge(require_once($testRoot.'/config/test.php'), require_once($envCfgFile));
$cfg = array_merge(require($testRoot . '/config/test.php'), require($envCfgFile));
}
}
// If no environment is set we have to load the default config
if(!isset($cfg)) {
$cfg = require($testRoot.'/config/test.php');
if (!isset($cfg)) {
$cfg = require($testRoot . '/config/test.php');
}
// We prefer the system enviroment setting over the configuration
if($humhubRoot != null) {
// We prefer the system environment setting over the configuration
if ($humhubRoot) {
$cfg['humhub_root'] = $humhubRoot;
} else {
// If no humhub_root is given we assume to be in /protected/humhub/modules/<module>/tests/codeception directory
$cfg['humhub_root'] = ($cfg['humhub_root'] != null) ? $cfg['humhub_root'] : $testRoot . '../../../../';
$cfg['humhub_root'] ??= $testRoot . '../../../../';
}
// Set some configurations and overwrite the humhub_root
if(isset($cfg['modules'])) {
\Codeception\Configuration::append(['humhub_modules' => $cfg['modules']]);
if (isset($cfg['modules'])) {
Configuration::append(['humhub_modules' => $cfg['modules']]);
}
if(isset($cfg['humhub_root'])) {
\Codeception\Configuration::append(['humhub_root' => $cfg['humhub_root']]);
if (isset($cfg['humhub_root'])) {
Configuration::append(['humhub_root' => $cfg['humhub_root']]);
}
if(isset($cfg['fixtures'])) {
\Codeception\Configuration::append(['fixtures' => $cfg['fixtures']]);
if (isset($cfg['fixtures'])) {
Configuration::append(['fixtures' => $cfg['fixtures']]);
}
return $cfg;

View File

@ -4,6 +4,7 @@ namespace tests\codeception\_support;
use Codeception\Module;
use humhub\modules\activity\tests\codeception\fixtures\ActivityFixture;
use humhub\modules\content\tests\codeception\fixtures\ContentContainerSettingFixture;
use humhub\modules\content\tests\codeception\fixtures\ContentFixture;
use humhub\modules\file\models\FileHistory;
use humhub\modules\file\tests\codeception\fixtures\FileFixture;
@ -125,6 +126,7 @@ class DynamicFixtureHelper extends Module
'url_oembed' => ['class' => UrlOembedFixture::class],
'group_permission' => ['class' => GroupPermissionFixture::class],
'settings' => ['class' => SettingFixture::class],
'contentcontainer_settings' => ['class' => ContentContainerSettingFixture::class],
'space' => [ 'class' => SpaceFixture::class],
'space_membership' => [ 'class' => SpaceMembershipFixture::class],
'content' => ['class' => ContentFixture::class],

View File

@ -2,14 +2,30 @@
namespace tests\codeception\_support;
use Codeception\Configuration;
use Codeception\Exception\ModuleException;
use Codeception\Module;
use Codeception\Module\Yii2;
use humhub\models\UrlOembed;
use humhub\modules\activity\tests\codeception\fixtures\ActivityFixture;
use humhub\modules\content\tests\codeception\fixtures\ContentContainerFixture;
use humhub\modules\content\tests\codeception\fixtures\ContentFixture;
use humhub\modules\content\widgets\richtext\converter\RichTextToHtmlConverter;
use humhub\modules\content\widgets\richtext\converter\RichTextToMarkdownConverter;
use humhub\modules\content\widgets\richtext\converter\RichTextToPlainTextConverter;
use humhub\modules\content\widgets\richtext\converter\RichTextToShortTextConverter;
use humhub\modules\file\tests\codeception\fixtures\FileFixture;
use humhub\modules\file\tests\codeception\fixtures\FileHistoryFixture;
use humhub\modules\friendship\tests\codeception\fixtures\FriendshipFixture;
use humhub\modules\live\tests\codeception\fixtures\LiveFixture;
use humhub\modules\notification\tests\codeception\fixtures\NotificationFixture;
use humhub\modules\space\tests\codeception\fixtures\SpaceFixture;
use humhub\modules\space\tests\codeception\fixtures\SpaceMembershipFixture;
use humhub\modules\user\tests\codeception\fixtures\GroupPermissionFixture;
use humhub\modules\user\tests\codeception\fixtures\UserFullFixture;
use humhub\tests\codeception\fixtures\SettingFixture;
use humhub\tests\codeception\fixtures\UrlOembedFixture;
use TypeError;
use Yii;
use yii\db\ActiveRecord;
use Codeception\Test\Unit;
@ -20,13 +36,16 @@ use humhub\modules\notification\models\Notification;
use humhub\modules\user\components\PermissionManager;
use humhub\modules\user\models\User;
use humhub\modules\friendship\models\Friendship;
use yii\db\Command;
use yii\db\Exception;
use yii\db\ExpressionInterface;
use yii\db\Query;
/**
* @SuppressWarnings(PHPMD)
*/
class HumHubDbTestCase extends Unit
{
protected $fixtureConfig;
public $appConfig = '@tests/codeception/config/unit.php';
@ -38,7 +57,7 @@ class HumHubDbTestCase extends Unit
{
parent::setUp();
$webRoot = dirname(dirname(__DIR__)) . '/../../..';
$webRoot = dirname(__DIR__, 2) . '/../../..';
Yii::setAlias('@webroot', realpath($webRoot));
$this->initModules();
$this->reloadSettings();
@ -83,19 +102,17 @@ class HumHubDbTestCase extends Unit
*/
protected function initModules()
{
$cfg = \Codeception\Configuration::config();
$cfg = Configuration::config();
if (!empty($cfg['humhub_modules'])) {
Yii::$app->moduleManager->enableModules($cfg['humhub_modules']);
}
}
/**
* @inheritdoc
*/
public function _fixtures()
/* @codingStandardsIgnoreLine PSR2.Methods.MethodDeclaration.Underscore */
public function _fixtures(): array
{
$cfg = \Codeception\Configuration::config();
$cfg = Configuration::config();
if (!$this->fixtureConfig && isset($cfg['fixtures'])) {
$this->fixtureConfig = $cfg['fixtures'];
@ -116,22 +133,22 @@ class HumHubDbTestCase extends Unit
return $result;
}
protected function getDefaultFixtures()
protected function getDefaultFixtures(): array
{
return [
'user' => ['class' => UserFullFixture::class],
'url_oembed' => ['class' => UrlOembedFixture::class],
'group_permission' => ['class' => \humhub\modules\user\tests\codeception\fixtures\GroupPermissionFixture::class],
'contentcontainer' => ['class' => \humhub\modules\content\tests\codeception\fixtures\ContentContainerFixture::class],
'settings' => ['class' => \humhub\tests\codeception\fixtures\SettingFixture::class],
'space' => ['class' => \humhub\modules\space\tests\codeception\fixtures\SpaceFixture::class],
'space_membership' => ['class' => \humhub\modules\space\tests\codeception\fixtures\SpaceMembershipFixture::class],
'content' => ['class' => \humhub\modules\content\tests\codeception\fixtures\ContentFixture::class],
'notification' => ['class' => \humhub\modules\notification\tests\codeception\fixtures\NotificationFixture::class],
'file' => ['class' => \humhub\modules\file\tests\codeception\fixtures\FileFixture::class],
'file_history' => ['class' => \humhub\modules\file\tests\codeception\fixtures\FileHistoryFixture::class],
'activity' => ['class' => \humhub\modules\activity\tests\codeception\fixtures\ActivityFixture::class],
'friendship' => ['class' => \humhub\modules\friendship\tests\codeception\fixtures\FriendshipFixture::class],
'group_permission' => ['class' => GroupPermissionFixture::class],
'contentcontainer' => ['class' => ContentContainerFixture::class],
'settings' => ['class' => SettingFixture::class],
'space' => ['class' => SpaceFixture::class],
'space_membership' => ['class' => SpaceMembershipFixture::class],
'content' => ['class' => ContentFixture::class],
'notification' => ['class' => NotificationFixture::class],
'file' => ['class' => FileFixture::class],
'file_history' => ['class' => FileHistoryFixture::class],
'activity' => ['class' => ActivityFixture::class],
'friendship' => ['class' => FriendshipFixture::class],
'live' => [ 'class' => LiveFixture::class]
];
}
@ -143,7 +160,7 @@ class HumHubDbTestCase extends Unit
'source_class' => $source->className(),
'source_pk' => $source->getPrimaryKey(),
]);
if(is_string($target_id)) {
if (is_string($target_id)) {
$msg = $target_id;
$target_id = null;
}
@ -152,7 +169,7 @@ class HumHubDbTestCase extends Unit
$notificationQuery->andWhere(['originator_user_id' => $originator_id]);
}
if($target_id != null) {
if ($target_id != null) {
$notificationQuery->andWhere(['user_id' => $target_id]);
}
@ -167,7 +184,7 @@ class HumHubDbTestCase extends Unit
$notificationQuery->andWhere(['originator_user_id' => $originator_id]);
}
if($target_id != null) {
if ($target_id != null) {
$notificationQuery->andWhere(['user_id' => $target_id]);
}
@ -182,7 +199,7 @@ class HumHubDbTestCase extends Unit
$notificationQuery->andWhere(['originator_user_id' => $originator_id]);
}
if($target_id != null) {
if ($target_id != null) {
$notificationQuery->andWhere(['user_id' => $target_id]);
}
@ -200,10 +217,11 @@ class HumHubDbTestCase extends Unit
}
/**
* @return \Codeception\Module\Yii2|\Codeception\Module
* @throws \Codeception\Exception\ModuleException
* @return Yii2|Module
* @throws ModuleException
*/
public function getYiiModule() {
public function getYiiModule()
{
return $this->getModule('Yii2');
}
@ -213,25 +231,28 @@ class HumHubDbTestCase extends Unit
*/
public function assertMailSent($count = 0)
{
return $this->getYiiModule()->seeEmailIsSent($count);
/** @noinspection PhpUnhandledExceptionInspection */
$this->getYiiModule()->seeEmailIsSent($count);
}
/**
* @param int $count
* @throws \Codeception\Exception\ModuleException
*
* @throws ModuleException
* @since 1.3
*/
public function assertSentEmail($count = 0)
public function assertSentEmail(int $count = 0)
{
return $this->getYiiModule()->seeEmailIsSent($count);
$this->getYiiModule()->seeEmailIsSent($count);
}
public function assertEqualsLastEmailTo($to, $strict = true)
{
if(is_string($to)) {
if (is_string($to)) {
$to = [$to];
}
/** @noinspection PhpUnhandledExceptionInspection */
$message = $this->getYiiModule()->grabLastSentEmail();
$expected = $message->getTo();
@ -239,22 +260,125 @@ class HumHubDbTestCase extends Unit
$this->assertArrayHasKey($email, $expected);
}
if($strict) {
$this->assertEquals(count($to), count($expected));
if ($strict) {
$this->assertCount(count($expected), $to);
}
}
public function assertEqualsLastEmailSubject($subject)
{
/** @noinspection PhpUnhandledExceptionInspection */
$message = $this->getYiiModule()->grabLastSentEmail();
$this->assertEquals($subject, str_replace(["\n", "\r"], '', $message->getSubject()));
}
/**
* @param int|null $expected Number of records expected. Null for any number, but not none
* @param string|array|ExpressionInterface $tables
* @param string|array|ExpressionInterface|null $condition
* @param array|null $params
* @param string $message
*
* @return void
* @since 1.15
*/
public function assertRecordCount(?int $expected, $tables, $condition = null, ?array $params = [], string $message = ''): void
{
$count = $this->dbCount($tables, $condition, $params ?? []);
if ($expected === null) {
$this->assertGreaterThan(0, $count, $message);
} else {
$this->assertEquals($expected, $count, $message);
}
}
/**
* @param string|array|ExpressionInterface $tables
* @param string|array|ExpressionInterface|null $condition
* @param array|null $params
* @param string $message
*
* @return void
* @since 1.15
*/
public function assertRecordExistsAny($tables, $condition = null, ?array $params = [], string $message = 'Record does not exist'): void
{
$this->assertRecordCount(null, $tables, $condition, $params ?? [], $message);
}
/**
* @param string|array|ExpressionInterface $tables
* @param string|array|ExpressionInterface|null $condition
* @param array|null $params
* @param string $message
*
* @return void
* @since 1.15
*/
public function assertRecordExists($tables, $condition = null, ?array $params = [], string $message = 'Record does not exist'): void
{
$this->assertRecordCount(1, $tables, $condition, $params ?? [], $message);
}
/**
* @param string|array|ExpressionInterface $tables
* @param string|array|ExpressionInterface|null $condition
* @param array|null $params
* @param string $message
*
* @return void
* @since 1.15
*/
public function assertRecordNotExists($tables, $condition = null, ?array $params = [], string $message = 'Record exists'): void
{
$this->assertRecordCount(0, $tables, $condition, $params ?? [], $message);
}
/**
* @param int|string|null $expected Number of records expected. Null for any number, but not none
* @param string $column
* @param string|array|ExpressionInterface $tables
* @param string|array|ExpressionInterface|null $condition
* @param array|null $params
* @param string $message
*
* @return void
* @since 1.15
*/
public function assertRecordValue($expected, string $column, $tables, $condition = null, ?array $params = [], string $message = ''): void
{
$value = $this->dbQuery($tables, $condition, $params, 1)->select($column)->scalar();
$this->assertEquals($expected, $value, $message);
}
public function expectExceptionTypeError(string $calledClass, string $method, int $argumentNumber, string $argumentName, string $expectedType, string $givenTye, string $exceptionClass = TypeError::class): void
{
$this->expectException($exceptionClass);
$calledClass = str_replace('\\', '\\\\', $calledClass);
$argumentName = ltrim($argumentName, '$');
$this->expectExceptionMessageRegExp(
sprintf(
// Php < 8 uses: "Argument n passed to class::method() ..."
// PHP > 7 uses: "class::method(): Argument #n ($argument) ..."
'@^((Argument %d passed to )?%s::%s\\(\\)(?(2)|: Argument #%d \\(\\$%s\\))) must be of( the)? type %s, %s given, called in /.*@',
$argumentNumber,
$calledClass,
$method,
$argumentNumber,
$argumentName,
$expectedType,
$givenTye
)
);
}
/**
* @param bool $allow
*/
public function allowGuestAccess($allow = true)
public function allowGuestAccess(bool $allow = true)
{
Yii::$app
->getModule('user')
@ -264,7 +388,7 @@ class HumHubDbTestCase extends Unit
public function setProfileField($field, $value, $user)
{
if(is_int($user)) {
if (is_int($user)) {
$user = User::findOne($user);
} elseif (is_string($user)) {
$user = User::findOne(['username' => $user]);
@ -311,7 +435,7 @@ class HumHubDbTestCase extends Unit
$contentContainer->permissionManager->clear();
}
public function becomeUser($userName)
public function becomeUser($userName): ?User
{
$user = User::findOne(['username' => $userName]);
Yii::$app->user->switchIdentity($user);
@ -320,7 +444,104 @@ class HumHubDbTestCase extends Unit
public function logout()
{
Yii::$app->user->logout(true);
Yii::$app->user->logout();
}
/**
* @see \yii\db\Connection::createCommand()
* @since 1.15
*/
public function dbCommand($sql = null, $params = []): Command
{
return Yii::$app->getDb()->createCommand($sql, $params);
}
/**
* @param Command $cmd
* @param bool $execute
*
* @return Command
* @throws Exception
*/
protected function dbCommandExecute(Command $cmd, bool $execute = true): Command
{
if ($execute) {
$cmd->execute();
}
return $cmd;
}
/**
* @see Query
* @since 1.15
*/
public function dbQuery($tables, $condition, $params = [], $limit = 10): Query
{
return (new Query())
->from($tables)
->where($condition, $params)
->limit($limit);
}
/**
* @see Command::insert
* @since 1.15
*/
public function dbInsert($table, $columns, bool $execute = true): Command
{
return $this->dbCommandExecute($this->dbCommand()->insert($table, $columns), $execute);
}
/**
* @see Command::update
* @since 1.15
*/
public function dbUpdate($table, $columns, $condition = '', $params = [], bool $execute = true): Command
{
return $this->dbCommandExecute($this->dbCommand()->update($table, $columns, $condition, $params), $execute);
}
/**
* @see Command::upsert
* @since 1.15
*/
public function dbUpsert($table, $insertColumns, $updateColumns = true, $params = [], bool $execute = true): Command
{
return $this->dbCommandExecute($this->dbCommand()->upsert($table, $insertColumns, $updateColumns, $params), $execute);
}
/**
* @see Command::delete()
* @since 1.15
*/
public function dbDelete($table, $condition = '', $params = [], bool $execute = true): Command
{
return $this->dbCommandExecute($this->dbCommand()->delete($table, $condition, $params), $execute);
}
/**
* @see Query::select
* @see Query::from
* @see Query::where
* @see \yii\db\QueryTrait::limit()
* @since 1.15
*/
public function dbSelect($tables, $columns, $condition = '', $params = [], $limit = 10, $selectOption = null): array
{
return $this->dbQuery($tables, $condition, $params, $limit)
->select($columns, $selectOption)
->all();
}
/**
* @see Command::delete()
* @since 1.15
*/
public function dbCount($tables, $condition = '', $params = [])
{
return $this->dbQuery($tables, $condition, $params)
->select("count(*)")
->scalar();
}
}

View File

@ -1,7 +1,14 @@
<?php
/*
* @link https://www.humhub.org/
* @copyright Copyright (c) 2023 HumHub GmbH & Co. KG
* @license https://www.humhub.com/licences
*/
namespace tests\codeception\_support;
use Codeception\Exception\ConfigurationException;
use Yii;
/**
@ -13,7 +20,7 @@ class HumHubTestConfiguration
* This function is used for retrieving the humhub configuration for
* a given test suite by merging default configuration with the test configuration and
* environment configuration of the user.
*
*
* @param type $suite
* @return type
*/
@ -22,50 +29,51 @@ class HumHubTestConfiguration
$config = self::initConfig($suite);
return self::mergeWithEnvironmentConfig($config, $suite);
}
/**
* Initializes the configuration for the given suite by merging
*
*
* @humhubTests/codeception/config/<suite>.php -> Default config for this suite
* @tests/config/common.php -> Common config of the current test module
* @test/config/<suite>.php -> Suite config of the current test module
*
*
* @param type $suite the given suite e.g acceptance/functional/unit
* @return type merged config
* @return array merged config
*/
private static function initConfig($suite)
private static function initConfig($suite): array
{
return \yii\helpers\ArrayHelper::merge(
// Default Test Config
require(Yii::getAlias('@humhubTests/codeception/config/'.$suite.'.php')),
require(Yii::getAlias('@humhubTests/codeception/config/' . $suite . '.php')),
// User Overwrite Common Config
require(Yii::getAlias('@tests/config/common.php')),
// User Overwrite Suite Config
require(Yii::getAlias('@tests/config/'.$suite.'.php'))
require(Yii::getAlias('@tests/config/' . $suite . '.php'))
);
}
/**
* Merges environmental configuration if existing.
* By running "codecept run functional --env myEnvironment" you can choose the execution environment
* and overwrite the default configuration in your @tests/config/env/myEnvironment directory.
*
*
* @param type $result
* @param type $cfg
* @param type $suite
*
* @return type
* @throws ConfigurationException
*/
private static function mergeWithEnvironmentConfig($result, $suite)
{
$cfg = \Codeception\Configuration::config();
// If a environment was set we use the first environment as execution environment and try including a environment specific cfg
if (isset($cfg['environment'])) {
$env = $cfg['environment'][0][0];
$envCfgCommonFile = Yii::getAlias('@env/'. $env .'/common.php');
$envCfgFile = Yii::getAlias('@env/'. $env .'/'.$suite. '.php');
$envCfgCommonFile = Yii::getAlias('@env/' . $env . '/common.php');
$envCfgFile = Yii::getAlias('@env/' . $env . '/' . $suite . '.php');
//Merge with common environment config
if (file_exists($envCfgCommonFile)) {
$result = \yii\helpers\ArrayHelper::merge(
@ -74,7 +82,7 @@ class HumHubTestConfiguration
require($envCfgCommonFile)
);
}
//Merge with suite envornment config
if (file_exists($envCfgFile)) {
$result = \yii\helpers\ArrayHelper::merge(
@ -84,7 +92,7 @@ class HumHubTestConfiguration
);
}
}
return $result;
}
}

View File

@ -14,7 +14,7 @@ $consoleConfig = [
]
];
$config = yii\helpers\ArrayHelper::merge(
$config = yii\helpers\ArrayHelper::merge(
// Common HumHub Config
require(YII_APP_BASE_PATH . '/humhub/config/common.php'),
// Console HumHub Config

View File

@ -24,7 +24,7 @@ $testConfig = [
],
'enablePjax' => true
],
];
defined('YII_APP_BASE_PATH') or define('YII_APP_BASE_PATH', dirname(dirname(dirname(dirname(__DIR__)))));

View File

@ -1,4 +1,11 @@
<?php
/*
* @link https://www.humhub.org/
* @copyright Copyright (c) 2023 HumHub GmbH & Co. KG
* @license https://www.humhub.com/licences
*/
return [
['name' => 'name', 'value' => 'HumHub', 'module_id' => 'base'],
['name' => 'baseUrl', 'value' => 'http://localhost:8080', 'module_id' => 'base'],
@ -67,4 +74,6 @@ return [
['name' => 'defaultLanguage', 'value' => 'en-US', 'module_id' => 'base'],
['name' => 'maintenanceMode', 'value' => '0', 'module_id' => 'base'],
['name' => 'enableProfilePermissions', 'value' => '1', 'module_id' => 'user'],
['name' => 'testSetting', 'value' => 'Test Setting for Base', 'module_id' => 'base' ],
['name' => 'testSetting0', 'value' => 'Test Setting 0 for Base', 'module_id' => 'base', ],
];

View File

@ -0,0 +1,21 @@
<?php
/*
* @link https://www.humhub.org/
* @copyright Copyright (c) 2023 HumHub GmbH & Co. KG
* @license https://www.humhub.com/licences
*/
namespace humhub\tests\codeception\unit\components;
use Codeception\Test\Unit;
use humhub\libs\BaseSettingsManager;
use tests\codeception\_support\HumHubDbTestCase;
class BaseSettingsManagerTest extends HumHubDbTestCase
{
public function testIsDatabaseInstalled()
{
$this->assertTrue(BaseSettingsManager::isDatabaseInstalled());
}
}

View File

@ -0,0 +1,44 @@
<?php
/*
* @link https://www.humhub.org/
* @copyright Copyright (c) 2023 HumHub GmbH & Co. KG
* @license https://www.humhub.com/licences
*/
namespace humhub\tests\codeception\unit\components;
use humhub\components\SettingActiveRecord;
use humhub\models\Setting;
use tests\codeception\_support\HumHubDbTestCase;
use yii\base\InvalidCallException;
class SettingActiveRecordTest extends HumHubDbTestCase
{
public function testGetCacheKeyFormat()
{
$this->assertEquals('settings-%s', SettingActiveRecord::CACHE_KEY_FORMAT, "Cache key format changed!");
}
public function testGetCacheKeyFields()
{
$this->assertEquals(['module_id'], SettingActiveRecord::CACHE_KEY_FIELDS, "Cache key format changed!");
}
public function testGetCacheKey()
{
$cacheKey = Setting::getCacheKey('test');
$this->assertEquals('settings-test', $cacheKey, "Cache key malformed!");
$cacheKey = Setting::getCacheKey('test', 'more');
$this->assertEquals('settings-test', $cacheKey, "Cache key malformed!");
}
public function testDeleteAll()
{
$this->expectException(InvalidCallException::class);
$this->expectExceptionMessageRegExp(sprintf('@%s@', str_replace('\\', '\\\\', SettingActiveRecord::class)));
SettingActiveRecord::deleteAll();
}
}

View File

@ -0,0 +1,43 @@
<?php
/*
* @link https://www.humhub.org/
* @copyright Copyright (c) 2023 HumHub GmbH & Co. KG
* @license https://www.humhub.com/licences
*/
namespace humhub\tests\codeception\unit\components;
use humhub\components\SettingsManager;
class SettingsManagerMock extends SettingsManager
{
public bool $usedFind = false;
protected function find()
{
$this->usedFind = true;
return parent::find();
}
public function getCacheKey(): string
{
return parent::getCacheKey();
}
/**
* @return bool
*/
public function didAccessDB(): bool
{
$read = $this->usedFind;
$this->usedFind = false;
return $read;
}
public function invalidateCache()
{
parent::invalidateCache();
}
}

View File

@ -0,0 +1,372 @@
<?php
/*
* @link https://www.humhub.org/
* @copyright Copyright (c) 2023 HumHub GmbH & Co. KG
* @license https://www.humhub.com/licences
*/
/** @noinspection MissedFieldInspection */
namespace humhub\tests\codeception\unit\components;
use humhub\components\SettingsManager;
use humhub\libs\BaseSettingsManager;
use humhub\models\Setting;
use humhub\modules\content\components\ContentContainerSettingsManager;
use humhub\modules\space\models\Space;
use humhub\modules\user\models\User;
use tests\codeception\_support\HumHubDbTestCase;
use Yii;
use yii\base\InvalidArgumentException;
use yii\base\InvalidConfigException;
use yii\caching\ArrayCache;
use yii\caching\DummyCache;
use yii\helpers\ArrayHelper;
use function PHPUnit\Framework\assertInstanceOf;
class SettingsManagerTest extends HumHubDbTestCase
{
protected $fixtureConfig = ['default'];
public function testCreateWithoutModuleId()
{
$this->expectException(InvalidConfigException::class);
$this->expectExceptionMessage('Module id not set!');
new SettingsManager();
}
public function testCreateWithEmptyModuleId()
{
$this->expectException(InvalidConfigException::class);
$this->expectExceptionMessage('Empty module id!');
$this->s = new SettingsManager(['moduleId' => '']);
}
public function testCreateForBaseModule()
{
$module = 'base';
$sm = new SettingsManager(['moduleId' => $module]);
$this->assertInstanceOf(SettingsManager::class, $sm);
}
public function testCreateForNonExistentModule()
{
$sm = new SettingsManager(['moduleId' => '_']);
$this->assertInstanceOf(SettingsManager::class, $sm);
}
public function testGetValuesForBaseModule()
{
$module = 'base';
$sm = new SettingsManager(['moduleId' => $module]);
$value = $sm->get('testSetting');
$this->assertEquals('Test Setting for Base', $value);
$value = $sm->get('testSetting_');
$this->assertNull($value);
}
public function testSpace()
{
$module = 'base';
$sm = new SettingsManager(['moduleId' => $module]);
$smSpace = $sm->space();
$this->assertNull($smSpace, "No Space Settings Manager should have been returned");
$space = Space::findOne(['id' => 1]);
$this->assertInstanceOf(Space::class, $space);
$smSpace = $sm->space($space);
$this->assertInstanceOf(
ContentContainerSettingsManager::class,
$smSpace,
"No Space Settings Manager was returned"
);
$this->assertEquals($module, $smSpace->moduleId);
$this->assertEquals($space, $smSpace->contentContainer);
}
public function testUser()
{
$module = 'base';
$sm = new SettingsManager(['moduleId' => $module]);
$smUser = $sm->user();
$this->assertNull($smUser, "No User Settings Manager should have been returned");
$user = $this->becomeUser('User2');
$smUser = $sm->user();
$this->assertInstanceOf(
ContentContainerSettingsManager::class,
$smUser,
"No User Settings Manager was returned"
);
$this->assertEquals($module, $smUser->moduleId);
$this->assertEquals($user, $smUser->contentContainer);
$user = User::findOne(['id' => 2]);
$this->assertInstanceOf(User::class, $user);
$smUser = $sm->user($user);
$this->assertInstanceOf(
ContentContainerSettingsManager::class,
$smUser,
"No User Settings Manager was returned"
);
$this->assertEquals($module, $smUser->moduleId);
$this->assertEquals($user, $smUser->contentContainer);
}
public function testSettingValues()
{
$module = 'base';
$table = Setting::tableName();
$sm = new SettingsManager(['moduleId' => $module]);
$this->assertEquals('Test Setting for Base', $sm->get('testSetting'));
$setting = 'testSetting';
$value = 'Hello World';
$sm->set($setting, $value);
$this->assertRecordExists($table, ['name' => $setting, 'module_id' => $module]);
$this->assertRecordValue($value, 'value', $table, ['name' => $setting, 'module_id' => $module]);
$this->assertEquals($value, $sm->get($setting));
$setting = 'testSetting_';
$value = 'Brave New World';
$this->assertRecordNotExists($table, ['name' => $setting, 'module_id' => $module]);
$this->assertNull($sm->get($setting));
$sm->set($setting, $value);
$this->assertRecordExists($table, ['name' => $setting, 'module_id' => $module]);
$this->assertRecordValue($value, 'value', $table, ['name' => $setting, 'module_id' => $module]);
$this->assertEquals($value, $sm->get($setting));
$this->expectExceptionTypeError(BaseSettingsManager::class, 'set', 1, '$name', 'string', 'null');
$sm->set(null, "NULL");
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessageRegExp('@Argument #1 \\(\\$name\\) passed to .* may not be an empty string!@');
$sm->set('', "null-length string");
$setting = ' ';
$value = 'just a space';
$sm->set($setting, $value);
$this->assertRecordExists($table, ['name' => $setting, 'module_id' => $module]);
$this->assertRecordValue($value, 'value', $table, ['name' => $setting, 'module_id' => $module]);
$this->assertEquals($value, $sm->get($setting));
}
public function testSerialized()
{
$module = 'base';
$table = Setting::tableName();
$sm = new SettingsManager(['moduleId' => $module]);
$setting = 'testJson';
$this->assertRecordNotExists($table, ['name' => $setting, 'module_id' => $module]);
$this->assertNull($sm->getSerialized($setting), "Setting should not exist");
$tests = [
'5' => 5,
'"5"' => "5",
'5.5' => 5.5,
'"simple text"' => 'simple text',
'null' => null,
'[]' => [],
'{}' => (object)[],
'[null,"simple text","text with \"quotes\"",5]' => [null, 'simple text', 'text with "quotes"', 5],
'{"0":null,"x":"simple text"}' => [null, 'x' => 'simple text'],
'{"x":null,"y":"simple text"}' => (object)['x' => null, 'y' => 'simple text'],
];
array_walk($tests, function ($value, $json) use ($setting, $sm, $table, $module) {
$sm->setSerialized($setting, $value);
$this->assertRecordExists($table, ['name' => $setting, 'module_id' => $module]);
$this->assertRecordValue($json, 'value', $table, ['name' => $setting, 'module_id' => $module]);
$this->assertEquals($json, $sm->get($setting));
if (is_object($value) || is_array($value) && ArrayHelper::isAssociative($value)) {
$object = (object)$value;
$this->assertEquals($object, $sm->getSerialized($setting, null, false), "testing json: $json");
$value = (array)$value;
}
$this->assertEquals($value, $sm->getSerialized($setting), "testing json: $json");
});
}
public function testDelete()
{
$module = 'base';
$table = Setting::tableName();
$sm = new SettingsManager(['moduleId' => $module]);
// delete by null value
$setting = 'testSetting';
$this->assertNotNull($sm->get($setting));
$sm->set($setting, null);
$this->assertNull($sm->get($setting));
// delete by delete()
$setting = 'testSetting0';
$this->assertRecordExists($table, ['name' => $setting, 'module_id' => $module]);
$this->assertNotNull($sm->get($setting));
$sm->delete($setting);
$this->assertRecordNotExists($table, ['name' => $setting, 'module_id' => $module]);
$this->assertNull($sm->get($setting));
}
public function testDeleteAll()
{
$module = 'base';
$table = Setting::tableName();
$sm = new SettingsManager(['moduleId' => $module]);
$setting = 'testSetting';
$setting1 = 'testSetting_1';
$setting2 = 'testSetting_2';
$sm->set($setting1, 'something');
$sm->set($setting2, 'something');
$this->assertNotNull($sm->get($setting), "testSetting should not found");
$this->assertNotNull($sm->get($setting1), "testSetting_1 was not created");
$this->assertNotNull($sm->get($setting2), "testSetting_1 was not created");
$this->assertRecordExists($table, ['name' => $setting, 'module_id' => $module]);
$this->assertRecordExists($table, ['name' => $setting1, 'module_id' => $module]);
$this->assertRecordExists($table, ['name' => $setting2, 'module_id' => $module]);
$sm->deleteAll('Setting_');
$this->assertNotNull($sm->get($setting), "testSetting should not have been deleted");
$this->assertNotNull($sm->get($setting1), "testSetting_1 should not have been deleted");
$this->assertNotNull($sm->get($setting2), "testSetting_2 should not have been deleted");
$this->assertRecordExists($table, ['name' => $setting, 'module_id' => $module]);
$this->assertRecordExists($table, ['name' => $setting1, 'module_id' => $module]);
$this->assertRecordExists($table, ['name' => $setting2, 'module_id' => $module]);
$sm->deleteAll('testSetting_');
$this->assertNotNull($sm->get($setting), "testSetting should not have been deleted");
$this->assertNull($sm->get($setting1), "testSetting_1 should have been deleted");
$this->assertNull($sm->get($setting2), "testSetting_2 should have been deleted");
$this->assertRecordExists($table, ['name' => $setting, 'module_id' => $module]);
$this->assertRecordNotExists($table, ['name' => $setting1, 'module_id' => $module]);
$this->assertRecordNotExists($table, ['name' => $setting2, 'module_id' => $module]);
$sm->deleteAll('%Setting%');
$this->assertNull($sm->get($setting), "testSetting should not have been deleted");
$this->assertRecordNotExists($table, ['name' => $setting, 'module_id' => $module]);
$this->assertRecordExistsAny($table, ['module_id' => $module]);
$sm->deleteAll();
$this->assertRecordNotExists($table, ['module_id' => $module]);
}
public function testGetCached()
{
$module = 'base';
$table = Setting::tableName();
// make sure, cache is disabled
$cache = Yii::$app->cache;
if (!$cache instanceof DummyCache) {
Yii::$app->set('cache', new DummyCache());
}
assertInstanceOf(DummyCache::class, $cache = Yii::$app->cache);
// No Cache
// initialize and load data from database
$sm = new SettingsManagerMock(['moduleId' => $module]);
$this->assertTrue($sm->didAccessDB());
// do it again. If there is a cache, then the database is not used. Since there is no cache, it is used again
$sm = new SettingsManagerMock(['moduleId' => $module]);
$this->assertTrue($sm->didAccessDB());
// Enable Cache
Yii::$app->set('cache', new ArrayCache());
assertInstanceOf(ArrayCache::class, $cache = Yii::$app->cache);
// Enabled Cache
// initialize and load data from database
$sm = new SettingsManagerMock(['moduleId' => $module]);
$this->assertTrue($sm->didAccessDB());
// check if cache is saved
$key = $sm->getCacheKey();
$this->assertTrue($cache->exists($key));
// do it again. If there is a cache, then the database is not used. Since there is no cache, it is used again
$sm = new SettingsManagerMock(['moduleId' => $module]);
$this->assertFalse($sm->didAccessDB());
$key = $sm->getCacheKey();
$this->assertTrue($cache->exists($key));
$setting = 'testSetting';
// reading a value should not access the db
$this->assertNotNull($sm->get($setting));
$this->assertFalse($sm->didAccessDB());
// however, writing a value should both
$value = 'some other value';
$sm->set($setting, $value);
// ... access the db
$this->assertTrue($sm->didAccessDB());
// ... and clear the cache
$this->assertFalse($cache->exists($key));
// DB should be updated, too
$this->assertRecordValue($value, 'value', $table, ['name' => $setting, 'module_id' => $module]);
// next read should still
$this->assertEquals($value, $sm->get($setting));
// ... not access db
$this->assertFalse($sm->didAccessDB());
// ... or create the cache
$this->assertFalse($cache->exists($key));
// changing the value behind the scenes
$value2 = 'third value';
$this->dbUpdate($table, ['value' => $value2], ['name' => $setting, 'module_id' => $module]);
$this->assertRecordValue($value2, 'value', $table, ['name' => $setting, 'module_id' => $module]);
// getting the value now should still show tho "old" value
$this->assertEquals($value, $sm->get($setting));
// reloading the settings should
$sm->reload();
// ... access the db
$this->assertTrue($sm->didAccessDB());
// ... and re-build the cache
$this->assertTrue($cache->exists($key));
// ... and show the updated value
$this->assertEquals($value2, $sm->get($setting));
// invalidating the cache
$sm->invalidateCache();
// ... not access db
$this->assertFalse($sm->didAccessDB());
// ... but clear the cache
$this->assertFalse($cache->exists($key));
}
}

View File

@ -0,0 +1,70 @@
<?php
/*
* @link https://www.humhub.org/
* @copyright Copyright (c) 2023 HumHub GmbH & Co. KG
* @license https://www.humhub.com/licences
*/
namespace humhub\tests\codeception\unit\models;
use humhub\models\Setting;
use humhub\tests\codeception\unit\components\SettingActiveRecordTest;
use yii\base\Exception;
class SettingTest extends SettingActiveRecordTest
{
protected $fixtureConfig = ['default'];
public function testGetCacheKeyFormat()
{
/** @noinspection PhpClassConstantAccessedViaChildClassInspection */
$this->assertEquals('settings-%s', Setting::CACHE_KEY_FORMAT, "Cache key format changed!");
}
public function testGetCacheKeyFields()
{
/** @noinspection PhpClassConstantAccessedViaChildClassInspection */
$this->assertEquals(['module_id'], Setting::CACHE_KEY_FIELDS, "Cache key format changed!");
}
public function testDeleteAll()
{
$settingBefore = Setting::findAll(['module_id' => 'base', 'name' => 'testSetting']);
$this->assertNotEmpty($settingBefore, "Setting 'testSetting' for 'base' not found.");
Setting::deleteAll(['module_id' => 'base', 'name' => 'testSetting']);
$settingAfter = Setting::findAll(['module_id' => 'base', 'name' => 'testSetting']);
$this->assertCount(0, $settingAfter, "Setting 'testSetting' for 'base' was not deleted.");
}
public function testDeprecatedFixModuleIdAndName()
{
$this->assertEquals(['foo', 'bar'], Setting::fixModuleIdAndName('foo', 'bar'), "Translation messed things up!");
$this->assertEquals(
['allowGuestAccess', 'user'],
Setting::fixModuleIdAndName('allowGuestAccess', 'authentication_internal'),
"Translation messed things up!"
);
}
public function testDeprecatedGetValidSetting()
{
$this->assertEquals('Test Setting for Base', Setting::get('testSetting', 'base'), "Invalid value returned!");
}
public function testDeprecatedGetSettingFromInvalidModule()
{
$this->expectException(Exception::class);
$this->expectExceptionMessage('Could not find module: this module does not exist');
Setting::get('testSetting', 'this module does not exist');
}
public function testDeprecatedGetInvalidSetting()
{
$this->assertNull(Setting::get('testSetting_', 'base'), "Invalid value returned!");
}
}