testing brought in
This commit is contained in:
@@ -18,7 +18,17 @@
|
|||||||
"cakephp/cakephp": "^5.0"
|
"cakephp/cakephp": "^5.0"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"phpunit/phpunit": "^10.1"
|
"cakedc/cakephp-phpstan": "^4.1",
|
||||||
|
"cakephp/bake": "^3.0.0",
|
||||||
|
"cakephp/cakephp-codesniffer": "^5.0",
|
||||||
|
"cakephp/debug_kit": "^5.0.0",
|
||||||
|
"dereuromark/cakephp-ide-helper": "^2.13",
|
||||||
|
"dereuromark/cakephp-test-helper": "^2.6",
|
||||||
|
"fig-r/psr2r-sniffer": "^2.7",
|
||||||
|
"josegonzalez/dotenv": "^4.0",
|
||||||
|
"php-collective/decimal-object": "^1.3",
|
||||||
|
"phpstan/phpstan": "^2.1",
|
||||||
|
"phpunit/phpunit": "^10.5.5 || ^11.1.3 || ^12.1"
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
@@ -28,7 +38,25 @@
|
|||||||
"autoload-dev": {
|
"autoload-dev": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
"CheeseCake\\Test\\": "tests/",
|
"CheeseCake\\Test\\": "tests/",
|
||||||
"Cake\\Test\\": "vendor/cakephp/cakephp/tests/"
|
"Cake\\Test\\": "vendor/cakephp/cakephp/tests/",
|
||||||
|
"TestApp\\": "tests/test_app/src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"post-install-cmd": "App\\Console\\Installer::postInstall",
|
||||||
|
"post-create-project-cmd": "App\\Console\\Installer::postInstall",
|
||||||
|
"check": [
|
||||||
|
"@test",
|
||||||
|
"@cs-check"
|
||||||
|
],
|
||||||
|
"test": "phpunit --colors=always",
|
||||||
|
"cs-check": "vendor/bin/phpcs --colors --parallel=16",
|
||||||
|
"cs-fix": "vendor/bin/phpcbf --colors --parallel=16",
|
||||||
|
"stan": "phpstan analyze"
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"allow-plugins": {
|
||||||
|
"dealerdirect/phpcodesniffer-composer-installer": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
20
phpcs.xml
Normal file
20
phpcs.xml
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<?xml version="1.0"?>
|
||||||
|
<ruleset name="plugin">
|
||||||
|
<arg value="nps"/>
|
||||||
|
|
||||||
|
<file>src/</file>
|
||||||
|
<file>tests/</file>
|
||||||
|
|
||||||
|
<exclude-pattern>/tests/test_files/</exclude-pattern>
|
||||||
|
<exclude-pattern>/tests/test_app/</exclude-pattern>
|
||||||
|
|
||||||
|
<rule ref="vendor/fig-r/psr2r-sniffer/PSR2R/ruleset.xml"/>
|
||||||
|
|
||||||
|
<rule ref="PSR1.Classes.ClassDeclaration.MissingNamespace">
|
||||||
|
<exclude-pattern>*/config/Migrations/*</exclude-pattern>
|
||||||
|
</rule>
|
||||||
|
<rule ref="PhpCollective.Classes.ClassFileName.NoMatch">
|
||||||
|
<exclude-pattern>*/config/Migrations/*</exclude-pattern>
|
||||||
|
</rule>
|
||||||
|
|
||||||
|
</ruleset>
|
||||||
10
phpstan.neon
Normal file
10
phpstan.neon
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
includes:
|
||||||
|
- vendor/cakedc/cakephp-phpstan/extension.neon
|
||||||
|
|
||||||
|
parameters:
|
||||||
|
level: 4
|
||||||
|
paths:
|
||||||
|
- src
|
||||||
|
bootstrapFiles:
|
||||||
|
- tests/bootstrap.php
|
||||||
|
treatPhpDocTypesAsCertain: false
|
||||||
@@ -1,22 +1,31 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" colors="true" processIsolation="false" stopOnFailure="false" bootstrap="tests/bootstrap.php" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.5/phpunit.xsd" cacheDirectory=".phpunit.cache">
|
<phpunit
|
||||||
<php>
|
colors="true"
|
||||||
<ini name="memory_limit" value="-1"/>
|
processIsolation="false"
|
||||||
<ini name="apc.enable_cli" value="1"/>
|
stopOnFailure="false"
|
||||||
</php>
|
bootstrap="tests/bootstrap.php"
|
||||||
<!-- Add any additional test suites you want to run here -->
|
>
|
||||||
<testsuites>
|
<php>
|
||||||
<testsuite name="CheeseCake">
|
<ini name="memory_limit" value="-1"/>
|
||||||
<directory>tests/TestCase/</directory>
|
<ini name="apc.enable_cli" value="1"/>
|
||||||
</testsuite>
|
<env name="FIXTURE_SCHEMA_METADATA" value="tests/schema.php"/>
|
||||||
</testsuites>
|
</php>
|
||||||
<!-- Setup the extension for fixtures -->
|
|
||||||
<extensions>
|
<!-- Add any additional test suites you want to run here -->
|
||||||
<bootstrap class="Cake\TestSuite\Fixture\Extension\PHPUnitExtension"/>
|
<testsuites>
|
||||||
</extensions>
|
<testsuite name="CakeAddresses">
|
||||||
<source>
|
<directory>tests/TestCase/</directory>
|
||||||
<include>
|
</testsuite>
|
||||||
<directory suffix=".php">src/</directory>
|
</testsuites>
|
||||||
</include>
|
|
||||||
</source>
|
<!-- Setup fixture extension -->
|
||||||
|
<extensions>
|
||||||
|
<bootstrap class="Cake\TestSuite\Fixture\Extension\PHPUnitExtension"/>
|
||||||
|
</extensions>
|
||||||
|
|
||||||
|
<source>
|
||||||
|
<include>
|
||||||
|
<directory suffix=".php">src/</directory>
|
||||||
|
</include>
|
||||||
|
</source>
|
||||||
</phpunit>
|
</phpunit>
|
||||||
|
|||||||
15
psalm.xml
Normal file
15
psalm.xml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0"?>
|
||||||
|
<psalm
|
||||||
|
errorLevel="2"
|
||||||
|
resolveFromConfigFile="true"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xmlns="https://getpsalm.org/schema/config"
|
||||||
|
xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
|
||||||
|
>
|
||||||
|
<projectFiles>
|
||||||
|
<directory name="src" />
|
||||||
|
<ignoreFiles>
|
||||||
|
<directory name="vendor" />
|
||||||
|
</ignoreFiles>
|
||||||
|
</projectFiles>
|
||||||
|
</psalm>
|
||||||
@@ -13,9 +13,9 @@ use Cake\Routing\RouteBuilder;
|
|||||||
/**
|
/**
|
||||||
* Plugin for CheeseCake
|
* Plugin for CheeseCake
|
||||||
*/
|
*/
|
||||||
class CheeseCakePlugin extends BasePlugin
|
class CheeseCakePlugin extends BasePlugin {
|
||||||
{
|
|
||||||
/**
|
/**
|
||||||
* Load all the plugin configuration and bootstrap logic.
|
* Load all the plugin configuration and bootstrap logic.
|
||||||
*
|
*
|
||||||
* The host application is provided as an argument. This allows you to load
|
* The host application is provided as an argument. This allows you to load
|
||||||
@@ -24,11 +24,10 @@ class CheeseCakePlugin extends BasePlugin
|
|||||||
* @param \Cake\Core\PluginApplicationInterface $app The host application
|
* @param \Cake\Core\PluginApplicationInterface $app The host application
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
public function bootstrap(PluginApplicationInterface $app): void
|
public function bootstrap(PluginApplicationInterface $app): void {
|
||||||
{
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add routes for the plugin.
|
* Add routes for the plugin.
|
||||||
*
|
*
|
||||||
* If your plugin has many routes and you would like to isolate them into a separate file,
|
* If your plugin has many routes and you would like to isolate them into a separate file,
|
||||||
@@ -37,48 +36,45 @@ class CheeseCakePlugin extends BasePlugin
|
|||||||
* @param \Cake\Routing\RouteBuilder $routes The route builder to update.
|
* @param \Cake\Routing\RouteBuilder $routes The route builder to update.
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
public function routes(RouteBuilder $routes): void
|
public function routes(RouteBuilder $routes): void {
|
||||||
{
|
parent::routes($routes);
|
||||||
parent::routes($routes);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add middleware for the plugin.
|
* Add middleware for the plugin.
|
||||||
*
|
*
|
||||||
* @param \Cake\Http\MiddlewareQueue $middlewareQueue The middleware queue to update.
|
* @param \Cake\Http\MiddlewareQueue $middlewareQueue The middleware queue to update.
|
||||||
* @return \Cake\Http\MiddlewareQueue
|
* @return \Cake\Http\MiddlewareQueue
|
||||||
*/
|
*/
|
||||||
public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue
|
public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue {
|
||||||
{
|
// Add your middlewares here
|
||||||
// Add your middlewares here
|
|
||||||
|
|
||||||
return $middlewareQueue;
|
return $middlewareQueue;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add commands for the plugin.
|
* Add commands for the plugin.
|
||||||
*
|
*
|
||||||
* @param \Cake\Console\CommandCollection $commands The command collection to update.
|
* @param \Cake\Console\CommandCollection $commands The command collection to update.
|
||||||
* @return \Cake\Console\CommandCollection
|
* @return \Cake\Console\CommandCollection
|
||||||
*/
|
*/
|
||||||
public function console(CommandCollection $commands): CommandCollection
|
public function console(CommandCollection $commands): CommandCollection {
|
||||||
{
|
// Add your commands here
|
||||||
// Add your commands here
|
|
||||||
|
|
||||||
$commands = parent::console($commands);
|
$commands = parent::console($commands);
|
||||||
|
|
||||||
return $commands;
|
return $commands;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register application container services.
|
* Register application container services.
|
||||||
*
|
*
|
||||||
|
* @link https://book.cakephp.org/4/en/development/dependency-injection.html#dependency-injection
|
||||||
* @param \Cake\Core\ContainerInterface $container The Container to update.
|
* @param \Cake\Core\ContainerInterface $container The Container to update.
|
||||||
* @return void
|
* @return void
|
||||||
* @link https://book.cakephp.org/4/en/development/dependency-injection.html#dependency-injection
|
|
||||||
*/
|
*/
|
||||||
public function services(ContainerInterface $container): void
|
public function services(ContainerInterface $container): void {
|
||||||
{
|
// Add your services here
|
||||||
// Add your services here
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,5 @@ namespace CheeseCake\Controller;
|
|||||||
|
|
||||||
use App\Controller\AppController as BaseController;
|
use App\Controller\AppController as BaseController;
|
||||||
|
|
||||||
class AppController extends BaseController
|
class AppController extends BaseController {
|
||||||
{
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,65 +6,66 @@ use Cake\Core\Configure;
|
|||||||
use Cake\ORM\Table;
|
use Cake\ORM\Table;
|
||||||
use Cake\ORM\TableRegistry;
|
use Cake\ORM\TableRegistry;
|
||||||
|
|
||||||
trait OverrideTableTrait
|
trait OverrideTableTrait {
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @var Table|null
|
|
||||||
*/
|
|
||||||
protected ?Table $_table = null;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @var \Cake\ORM\Table|null
|
||||||
|
*/
|
||||||
|
protected ?Table $_table = null;
|
||||||
|
|
||||||
|
/**
|
||||||
* This object's default table alias.
|
* This object's default table alias.
|
||||||
*
|
*
|
||||||
* @var string|null
|
* @var string|null
|
||||||
*/
|
*/
|
||||||
protected ?string $defaultTable = null;
|
protected ?string $defaultTable = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var string
|
* @var string
|
||||||
*/
|
*/
|
||||||
protected string $_tableConfigKey = '';
|
protected string $_tableConfigKey = '';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the table instance
|
* Gets the table instance
|
||||||
*
|
*
|
||||||
* @return Table
|
* @return \Cake\ORM\Table
|
||||||
*/
|
*/
|
||||||
public function getTable(string|null $tableName)
|
public function getTable(string|null $tableName) {
|
||||||
{
|
if ($this->_table instanceof Table) {
|
||||||
if ($this->_table instanceof Table) {
|
return $this->_table;
|
||||||
return $this->_table;
|
}
|
||||||
}
|
$this->getTableConfigKey();
|
||||||
$this->getTableConfigKey();
|
$table = $tableName;
|
||||||
$table = $tableName;
|
if (!isset($table)) {
|
||||||
if (!isset($table)) {
|
$table = $this->defaultTable;
|
||||||
$table = $this->defaultTable;
|
if (Configure::read($this->_tableConfigKey)) {
|
||||||
if (Configure::read($this->_tableConfigKey)) {
|
$table = Configure::read($this->_tableConfigKey);
|
||||||
$table = Configure::read($this->_tableConfigKey);
|
}
|
||||||
}
|
}
|
||||||
}
|
$this->_table = TableRegistry::getTableLocator()->get($table);
|
||||||
$this->_table = TableRegistry::getTableLocator()->get($table);
|
|
||||||
|
|
||||||
return $this->_table;
|
return $this->_table;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function getTableConfigKey()
|
/**
|
||||||
{
|
* @return string
|
||||||
if (!$this->_tableConfigKey) {
|
*/
|
||||||
$this->_tableConfigKey = $this->getPlugin() . '.' . $this->defaultTable . '.table';
|
protected function getTableConfigKey() {
|
||||||
}
|
if (!$this->_tableConfigKey) {
|
||||||
|
$this->_tableConfigKey = $this->getPlugin() . '.' . $this->defaultTable . '.table';
|
||||||
|
}
|
||||||
|
|
||||||
return $this->_tableConfigKey;
|
return $this->_tableConfigKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the users table
|
* Set the users table
|
||||||
*
|
*
|
||||||
* @param Table $table table
|
* @param \Cake\ORM\Table $table table
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
public function setTable(Table $table)
|
public function setTable(Table $table) {
|
||||||
{
|
$this->_table = $table;
|
||||||
$this->_table = $table;
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -8,9 +8,9 @@ use Cake\ORM\Entity;
|
|||||||
/**
|
/**
|
||||||
* Secure Entity - must explicitly pass fields that are acceptable to be updated on each newEntity/patchEntity call
|
* Secure Entity - must explicitly pass fields that are acceptable to be updated on each newEntity/patchEntity call
|
||||||
*/
|
*/
|
||||||
class SecureEntity extends Entity
|
class SecureEntity extends Entity {
|
||||||
{
|
|
||||||
/**
|
/**
|
||||||
* Fields that can be mass assigned using newEntity() or patchEntity().
|
* Fields that can be mass assigned using newEntity() or patchEntity().
|
||||||
*
|
*
|
||||||
* Note that when '*' is set to true, this allows all unspecified fields to
|
* Note that when '*' is set to true, this allows all unspecified fields to
|
||||||
@@ -19,5 +19,6 @@ class SecureEntity extends Entity
|
|||||||
*
|
*
|
||||||
* @var array<string, bool>
|
* @var array<string, bool>
|
||||||
*/
|
*/
|
||||||
protected array $_accessible = [];
|
protected array $_accessible = [];
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,120 +3,163 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace CheeseCake\View\Helper;
|
namespace CheeseCake\View\Helper;
|
||||||
|
|
||||||
use Cake\Log\Log;
|
|
||||||
use Cake\Routing\Router;
|
use Cake\Routing\Router;
|
||||||
use Cake\View\Helper;
|
use Cake\View\Helper;
|
||||||
use Cake\View\View;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ActiveLink helper
|
* ActiveLink helper
|
||||||
*/
|
*/
|
||||||
class ActiveLinkHelper extends Helper
|
class ActiveLinkHelper extends Helper {
|
||||||
{
|
|
||||||
/**
|
/**
|
||||||
* Default configuration.
|
* Default configuration.
|
||||||
*
|
*
|
||||||
* @var array<string, mixed>
|
* @var array<string, mixed>
|
||||||
*/
|
*/
|
||||||
protected array $_defaultConfig = [
|
protected array $_defaultConfig = [
|
||||||
'activeClass' => 'active',
|
'activeClass' => 'active',
|
||||||
];
|
];
|
||||||
/**
|
|
||||||
|
/**
|
||||||
* List of helpers used by this helper
|
* List of helpers used by this helper
|
||||||
*
|
*
|
||||||
* @var string[]
|
* @var string[]
|
||||||
*/
|
*/
|
||||||
protected array $helpers = [
|
protected array $helpers = [
|
||||||
'Html',
|
'Html',
|
||||||
'Url',
|
'Url',
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array|string $title
|
* @param array|string $title
|
||||||
* @param array|string|null $url
|
* @param array|string|null $url
|
||||||
* @param array $options
|
* @param array $options
|
||||||
*
|
*
|
||||||
* @return string
|
* @return string
|
||||||
*/
|
*/
|
||||||
public function link(array|string $title, array|string|null $url = null, array $options = []): string
|
public function link(array|string $title, array|string|null $url = null, array $options = []): string {
|
||||||
{
|
$currentUrl = $options['current'] ?? Router::parseRequest($this->getView()->getRequest());
|
||||||
$currentUrl = Router::parseRequest($this->getView()->getRequest());
|
if (!array_key_exists('target', $options) || !$currentUrl) {
|
||||||
if (!array_key_exists('target', $options) || !$currentUrl) {
|
return $this->Html->link($title, $url, $options);
|
||||||
return $this->Html->link($title, $url, $options);
|
}
|
||||||
}
|
$target = $options['target'];
|
||||||
$target = $options['target'];
|
$activeClass = $options['activeClass'] ?? $this->getConfig('activeClass');
|
||||||
|
|
||||||
unset($options['target']);
|
unset($options['target']);
|
||||||
if (is_string($target)) {
|
unset($options['activeClass']);
|
||||||
return $this->_linkFromStringTarget($currentUrl, $target, $title, $url, $options);
|
|
||||||
}
|
|
||||||
if (!is_array($target)) {
|
|
||||||
return $this->Html->link($title, $url, $options);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!array_key_exists('plugin', $currentUrl)) {
|
if (is_string($target)) {
|
||||||
$currentUrl['plugin'] = false;
|
return $this->_linkFromStringTarget($currentUrl, $target, $title, $url, $activeClass, $options);
|
||||||
}
|
}
|
||||||
if (!array_key_exists('prefix', $currentUrl)) {
|
if (!is_array($target)) {
|
||||||
$currentUrl['prefix'] = false;
|
return $this->Html->link($title, $url, $options);
|
||||||
}
|
}
|
||||||
if (isset($target['or']) && $target['or']) {
|
|
||||||
foreach ($target['or'] as $singleTargetToMatch) {
|
|
||||||
if ($this->_matchesUrlFromArrayTarget($currentUrl, $singleTargetToMatch)) {
|
|
||||||
$options['class'] = $this->_addClass($options);
|
|
||||||
|
|
||||||
return $this->Html->link($title, $url, $options);
|
if (!array_key_exists('plugin', $currentUrl)) {
|
||||||
}
|
$currentUrl['plugin'] = false;
|
||||||
}
|
}
|
||||||
return $this->Html->link($title, $url, $options);
|
if (!array_key_exists('prefix', $currentUrl)) {
|
||||||
}
|
$currentUrl['prefix'] = false;
|
||||||
|
}
|
||||||
|
if (isset($target['or']) && $target['or']) {
|
||||||
|
foreach ($target['or'] as $singleTargetToMatch) {
|
||||||
|
if ($this->_matchesUrlFromArrayTarget($currentUrl, $singleTargetToMatch)) {
|
||||||
|
$options['class'] = $this->_addClass($options, $activeClass);
|
||||||
|
|
||||||
if (!$this->_matchesUrlFromArrayTarget($currentUrl, $target)) {
|
return $this->Html->link($title, $url, $options);
|
||||||
return $this->Html->link($title, $url, $options);
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$options['class'] = $this->_addClass($options);
|
return $this->Html->link($title, $url, $options);
|
||||||
|
}
|
||||||
|
|
||||||
return $this->Html->link($title, $url, $options);
|
if (!$this->doesUrlMatchTarget($target, $currentUrl)) {
|
||||||
}
|
return $this->Html->link($title, $url, $options);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
$options['class'] = $this->_addClass($options, $activeClass);
|
||||||
|
|
||||||
|
return $this->Html->link($title, $url, $options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array|string $targetUrl
|
||||||
|
* @param array|null $current |null current url
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function doesUrlMatchTarget(array|string $targetUrl, array|null $current = null) {
|
||||||
|
if (!isset($current)) {
|
||||||
|
$current = Router::parseRequest($this->getView()->getRequest());
|
||||||
|
}
|
||||||
|
if (is_string($targetUrl) && Router::normalize($current) == Router::normalize($targetUrl)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (is_array($targetUrl)) {
|
||||||
|
if (isset($targetUrl['or']) && $targetUrl['or']) {
|
||||||
|
foreach ($targetUrl['or'] as $singleTargetToMatch) {
|
||||||
|
$matched = $this->_matchesUrlFromArrayTarget($current, $singleTargetToMatch);
|
||||||
|
if ($matched) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->_matchesUrlFromArrayTarget($current, $targetUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
* @param array $providedOptions
|
* @param array $providedOptions
|
||||||
*
|
*
|
||||||
* @return string
|
* @return string
|
||||||
*/
|
*/
|
||||||
protected function _addClass(array $providedOptions): string
|
protected function _addClass(array $providedOptions, string $toAdd): string {
|
||||||
{
|
return array_key_exists('class', $providedOptions) ? $providedOptions['class'] . ' ' . $toAdd : $toAdd;
|
||||||
$activeClass = array_key_exists('activeClass', $providedOptions) ? $providedOptions['activeClass'] : $this->getConfig('activeClass');
|
}
|
||||||
|
|
||||||
return array_key_exists('class', $providedOptions) ? $providedOptions['class'] . ' ' . $activeClass : $activeClass;
|
/**
|
||||||
}
|
* @param array $current
|
||||||
|
* @param string $targetString
|
||||||
|
* @param string $title
|
||||||
|
* @param array|string|null $url
|
||||||
|
* @param array $options
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function _linkFromStringTarget(array $current, string $targetString, string $title, array|string|null $url, string $activeClass, array $options) {
|
||||||
|
if (Router::normalize($current) == Router::normalize($targetString)) {
|
||||||
|
$options['class'] = $this->_addClass($options, $activeClass);
|
||||||
|
|
||||||
protected function _linkFromStringTarget(array $current, string $targetString, string $title, array|string|null $url, array $options)
|
return $this->Html->link($title, $url, $options);
|
||||||
{
|
}
|
||||||
if (Router::normalize($current) == Router::normalize($targetString)) {
|
|
||||||
$options['class'] = $this->_addClass($options);
|
|
||||||
|
|
||||||
return $this->Html->link($title, $url, $options);
|
return $this->Html->link($title, $url, $options);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->Html->link($title, $url, $options);
|
/**
|
||||||
}
|
* @param array $current
|
||||||
|
* @param array $targetUrl
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
protected function _matchesUrlFromArrayTarget(array $current, array $targetUrl) {
|
||||||
|
foreach ($targetUrl as $targetKey => $targetValue) {
|
||||||
|
if (is_array($targetValue)) {
|
||||||
|
if (!in_array($current[$targetKey], $targetValue)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
protected function _matchesUrlFromArrayTarget(array $current, array $targetUrl)
|
continue;
|
||||||
{
|
}
|
||||||
foreach ($targetUrl as $targetKey => $targetValue) {
|
if (!array_key_exists($targetKey, $current) || $targetValue != $current[$targetKey]) {
|
||||||
if (is_array($targetValue)) {
|
return false;
|
||||||
if (!in_array($current[$targetKey], $targetValue)) {
|
}
|
||||||
return false;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
continue;
|
return true;
|
||||||
}
|
}
|
||||||
if (!array_key_exists($targetKey, $current) || $targetValue != $current[$targetKey]) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
118
tests/TestCase/View/Helper/ActiveLinkHelperTest.php
Normal file
118
tests/TestCase/View/Helper/ActiveLinkHelperTest.php
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace CheeseCake\Test\TestCase\View\Helper;
|
||||||
|
|
||||||
|
use Cake\Http\ServerRequest;
|
||||||
|
use Cake\Routing\Route\DashedRoute;
|
||||||
|
use Cake\Routing\RouteBuilder;
|
||||||
|
use Cake\Routing\Router;
|
||||||
|
use Cake\TestSuite\IntegrationTestTrait;
|
||||||
|
use Cake\TestSuite\TestCase;
|
||||||
|
use Cake\View\View;
|
||||||
|
use CheeseCake\View\Helper\ActiveLinkHelper;
|
||||||
|
|
||||||
|
class ActiveLinkHelperTest extends TestCase {
|
||||||
|
|
||||||
|
use IntegrationTestTrait;
|
||||||
|
public function setUp(): void {
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
// $this->loadRoutes();
|
||||||
|
$routeBuilder = Router::createRouteBuilder('/');
|
||||||
|
$routeBuilder->scope('/', function (RouteBuilder $routes) {
|
||||||
|
$routes->setRouteClass(DashedRoute::class);
|
||||||
|
$routes->get(
|
||||||
|
'/',
|
||||||
|
['controller' => 'Tests', 'action' => 'index'],
|
||||||
|
);
|
||||||
|
$routes->get(
|
||||||
|
'/admin/users',
|
||||||
|
['prefix' => 'Admin', 'controller' => 'Users', 'action' => 'index'],
|
||||||
|
);
|
||||||
|
$routes->get(
|
||||||
|
'/admin/users/view',
|
||||||
|
['prefix' => 'Admin', 'controller' => 'Users', 'action' => 'view'],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testLinkMatchesString(): void {
|
||||||
|
$request = new ServerRequest(['url' => '/']);
|
||||||
|
$view = new View($request);
|
||||||
|
$helper = new ActiveLinkHelper($view);
|
||||||
|
|
||||||
|
$result = $helper->link('goto', '/', [
|
||||||
|
'target' => '/',
|
||||||
|
'activeClass' => 'awesome-active-class',
|
||||||
|
]);
|
||||||
|
$this->assertStringContainsString('awesome-active-class', $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testLinkMatchesArrayFromCurrent(): void {
|
||||||
|
$request = new ServerRequest();
|
||||||
|
$view = new View($request);
|
||||||
|
$helper = new ActiveLinkHelper($view);
|
||||||
|
|
||||||
|
// matches specific controller action
|
||||||
|
$specificActionResult = $helper->link('goto', ['prefix' => 'Admin', 'controller' => 'Users', 'action' => 'index'], [
|
||||||
|
'target' => ['prefix' => 'Admin', 'controller' => 'Users', 'action' => 'index'],
|
||||||
|
'current' => ['prefix' => 'Admin', 'controller' => 'Users', 'action' => 'index'],
|
||||||
|
'activeClass' => 'awesome-active-class',
|
||||||
|
]);
|
||||||
|
$this->assertStringContainsString('awesome-active-class', $specificActionResult);
|
||||||
|
|
||||||
|
// match whole controller
|
||||||
|
$wholeControllerResult = $helper->link('goto', [
|
||||||
|
'plugin' => null,
|
||||||
|
'prefix' => 'Admin',
|
||||||
|
'controller' => 'Users',
|
||||||
|
'action' => 'index',
|
||||||
|
], [
|
||||||
|
'current' => ['prefix' => 'Admin', 'controller' => 'Users', 'action' => 'index'],
|
||||||
|
'target' => ['prefix' => 'Admin', 'controller' => 'Users'],
|
||||||
|
'activeClass' => 'awesome-active-class',
|
||||||
|
]);
|
||||||
|
$this->assertStringContainsString('awesome-active-class', $wholeControllerResult);
|
||||||
|
|
||||||
|
// match whole prefix
|
||||||
|
$wholePrefixResult = $helper->link('goto', [
|
||||||
|
'prefix' => 'Admin',
|
||||||
|
'controller' => 'Users',
|
||||||
|
'action' => 'index',
|
||||||
|
], [
|
||||||
|
'current' => ['prefix' => 'Admin', 'controller' => 'Users', 'action' => 'index'],
|
||||||
|
'target' => ['prefix' => 'Admin'],
|
||||||
|
'activeClass' => 'awesome-active-class',
|
||||||
|
]);
|
||||||
|
$this->assertStringContainsString('awesome-active-class', $wholePrefixResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testLinkDoesNotMatchArray(): void {
|
||||||
|
$request = new ServerRequest();
|
||||||
|
$request = $request->withParam('prefix', 'Admin');
|
||||||
|
$request = $request->withParam('controller', 'Users');
|
||||||
|
$request = $request->withParam('action', 'index');
|
||||||
|
$view = new View($request);
|
||||||
|
$helper = new ActiveLinkHelper($view);
|
||||||
|
|
||||||
|
// matches specific controller action
|
||||||
|
$specificActionResult = $helper->link('goto', ['prefix' => 'Admin', 'controller' => 'Users', 'action' => 'index'], [
|
||||||
|
'target' => ['controller' => 'Events', 'action' => 'index'],
|
||||||
|
'current' => ['prefix' => 'Admin', 'controller' => 'Users', 'action' => 'index'],
|
||||||
|
'activeClass' => 'awesome-active-class',
|
||||||
|
'class' => 'link-class',
|
||||||
|
]);
|
||||||
|
$this->assertStringNotContainsString('awesome-active-class', $specificActionResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,6 +1,27 @@
|
|||||||
<?php
|
<?php
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Cake\Core\Configure;
|
||||||
|
|
||||||
|
//use Cake\TestSuite\Fixture\SchemaLoader;
|
||||||
|
|
||||||
|
define('PLUGIN_ROOT', dirname(__DIR__));
|
||||||
|
define('ROOT', PLUGIN_ROOT . DS . 'tests' . DS . 'test_app');
|
||||||
|
define('TMP', PLUGIN_ROOT . DS . 'tmp' . DS);
|
||||||
|
define('LOGS', TMP . 'logs' . DS);
|
||||||
|
define('CACHE', TMP . 'cache' . DS);
|
||||||
|
define('APP', ROOT . DS . 'src' . DS);
|
||||||
|
define('APP_DIR', 'src');
|
||||||
|
define('CAKE_CORE_INCLUDE_PATH', PLUGIN_ROOT . '/vendor/cakephp/cakephp');
|
||||||
|
define('CORE_PATH', CAKE_CORE_INCLUDE_PATH . DS);
|
||||||
|
define('CAKE', CORE_PATH . APP_DIR . DS);
|
||||||
|
|
||||||
|
define('WWW_ROOT', PLUGIN_ROOT . DS . 'webroot' . DS);
|
||||||
|
define('TESTS', __DIR__ . DS);
|
||||||
|
define('CONFIG', TESTS . 'config' . DS);
|
||||||
|
|
||||||
|
ini_set('intl.default_locale', 'en-US');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test suite bootstrap for CheeseCakePlugin.
|
* Test suite bootstrap for CheeseCakePlugin.
|
||||||
*
|
*
|
||||||
@@ -9,15 +30,15 @@ declare(strict_types=1);
|
|||||||
* installed as a dependency of an application.
|
* installed as a dependency of an application.
|
||||||
*/
|
*/
|
||||||
$findRoot = function ($root) {
|
$findRoot = function ($root) {
|
||||||
do {
|
do {
|
||||||
$lastRoot = $root;
|
$lastRoot = $root;
|
||||||
$root = dirname($root);
|
$root = dirname($root);
|
||||||
if (is_dir($root . '/vendor/cakephp/cakephp')) {
|
if (is_dir($root . '/vendor/cakephp/cakephp')) {
|
||||||
return $root;
|
return $root;
|
||||||
}
|
}
|
||||||
} while ($root !== $lastRoot);
|
} while ($root !== $lastRoot);
|
||||||
|
|
||||||
throw new Exception('Cannot find the root of the application, unable to run tests');
|
throw new Exception('Cannot find the root of the application, unable to run tests');
|
||||||
};
|
};
|
||||||
$root = $findRoot(__FILE__);
|
$root = $findRoot(__FILE__);
|
||||||
unset($findRoot);
|
unset($findRoot);
|
||||||
@@ -34,11 +55,21 @@ require_once $root . '/vendor/autoload.php';
|
|||||||
require_once $root . '/vendor/cakephp/cakephp/tests/bootstrap.php';
|
require_once $root . '/vendor/cakephp/cakephp/tests/bootstrap.php';
|
||||||
|
|
||||||
if (file_exists($root . '/config/bootstrap.php')) {
|
if (file_exists($root . '/config/bootstrap.php')) {
|
||||||
require $root . '/config/bootstrap.php';
|
require $root . '/config/bootstrap.php';
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
Configure::write('App', [
|
||||||
|
'namespace' => 'TestApp',
|
||||||
|
'encoding' => 'UTF-8',
|
||||||
|
'paths' => [
|
||||||
|
'testWebroot' => PLUGIN_ROOT . DS . 'tests' . DS . 'test_app' . DS . 'webroot' . DS,
|
||||||
|
'webroot' => PLUGIN_ROOT . DS . 'webroot' . DS,
|
||||||
|
'templates' => [
|
||||||
|
PLUGIN_ROOT . DS . 'tests' . DS . 'test_app' . DS . 'templates' . DS,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]);
|
||||||
/**
|
/**
|
||||||
* Load schema from a SQL dump file.
|
* Load schema from a SQL dump file.
|
||||||
*
|
*
|
||||||
@@ -49,7 +80,6 @@ if (file_exists($root . '/config/bootstrap.php')) {
|
|||||||
* using migrations to provide schema for your plugin,
|
* using migrations to provide schema for your plugin,
|
||||||
* and using \Migrations\TestSuite\Migrator to load schema.
|
* and using \Migrations\TestSuite\Migrator to load schema.
|
||||||
*/
|
*/
|
||||||
use Cake\TestSuite\Fixture\SchemaLoader;
|
|
||||||
|
|
||||||
// Load a schema dump file.
|
// Load a schema dump file.
|
||||||
(new SchemaLoader())->loadSqlFiles('tests/schema.sql', 'test');
|
//(new SchemaLoader())->loadSqlFiles('tests/schema.sql', 'test');
|
||||||
|
|||||||
1
tests/test_app/config/bootstrap.php
Normal file
1
tests/test_app/config/bootstrap.php
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<?php
|
||||||
95
tests/test_app/config/routes.php
Normal file
95
tests/test_app/config/routes.php
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Routes configuration.
|
||||||
|
*
|
||||||
|
* In this file, you set up routes to your controllers and their actions.
|
||||||
|
* Routes are very important mechanism that allows you to freely connect
|
||||||
|
* different URLs to chosen controllers and their actions (functions).
|
||||||
|
*
|
||||||
|
* It's loaded within the context of `Application::routes()` method which
|
||||||
|
* receives a `RouteBuilder` instance `$routes` as method argument.
|
||||||
|
*
|
||||||
|
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
|
||||||
|
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
|
||||||
|
*
|
||||||
|
* Licensed under The MIT License
|
||||||
|
* For full copyright and license information, please see the LICENSE.txt
|
||||||
|
* Redistributions of files must retain the above copyright notice.
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
|
||||||
|
* @link https://cakephp.org CakePHP(tm) Project
|
||||||
|
* @license https://opensource.org/licenses/mit-license.php MIT License
|
||||||
|
*/
|
||||||
|
|
||||||
|
use Cake\Routing\Route\DashedRoute;
|
||||||
|
use Cake\Routing\RouteBuilder;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is loaded in the context of the `Application` class.
|
||||||
|
* So you can use `$this` to reference the application class instance
|
||||||
|
* if required.
|
||||||
|
*/
|
||||||
|
return function (RouteBuilder $routes): void {
|
||||||
|
/*
|
||||||
|
* The default class to use for all routes
|
||||||
|
*
|
||||||
|
* The following route classes are supplied with CakePHP and are appropriate
|
||||||
|
* to set as the default:
|
||||||
|
*
|
||||||
|
* - Route
|
||||||
|
* - InflectedRoute
|
||||||
|
* - DashedRoute
|
||||||
|
*
|
||||||
|
* If no call is made to `Router::defaultRouteClass()`, the class used is
|
||||||
|
* `Route` (`Cake\Routing\Route\Route`)
|
||||||
|
*
|
||||||
|
* Note that `Route` does not do any inflections on URLs which will result in
|
||||||
|
* inconsistently cased URLs when used with `{plugin}`, `{controller}` and
|
||||||
|
* `{action}` markers.
|
||||||
|
*/
|
||||||
|
$routes->setRouteClass(DashedRoute::class);
|
||||||
|
|
||||||
|
$routes->scope('/', function (RouteBuilder $builder): void {
|
||||||
|
/*
|
||||||
|
* Here, we are connecting '/' (base path) to a controller called 'Pages',
|
||||||
|
* its action called 'display', and we pass a param to select the view file
|
||||||
|
* to use (in this case, templates/Pages/home.php)...
|
||||||
|
*/
|
||||||
|
$builder->connect('/', ['controller' => 'Pages', 'action' => 'display', 'home']);
|
||||||
|
|
||||||
|
$builder->prefix('Admin', function (RouteBuilder $adminRouteBuilder): void {
|
||||||
|
$adminRouteBuilder->connect('/users/view', ['controller' => 'Pages', 'action' => 'view']);
|
||||||
|
$adminRouteBuilder->connect('/users/index', ['controller' => 'Pages', 'action' => 'index']);
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Connect catchall routes for all controllers.
|
||||||
|
*
|
||||||
|
* The `fallbacks` method is a shortcut for
|
||||||
|
*
|
||||||
|
* ```
|
||||||
|
* $builder->connect('/{controller}', ['action' => 'index']);
|
||||||
|
* $builder->connect('/{controller}/{action}/*', []);
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* You can remove these routes once you've connected the
|
||||||
|
* routes you want in your application.
|
||||||
|
*/
|
||||||
|
$builder->fallbacks();
|
||||||
|
});
|
||||||
|
/*
|
||||||
|
* If you need a different set of middleware or none at all,
|
||||||
|
* open new scope and define routes there.
|
||||||
|
*
|
||||||
|
* ```
|
||||||
|
* $routes->scope('/api', function (RouteBuilder $builder): void {
|
||||||
|
* // No $builder->applyMiddleware() here.
|
||||||
|
*
|
||||||
|
* // Parse specified extensions from URLs
|
||||||
|
* // $builder->setExtensions(['json', 'xml']);
|
||||||
|
*
|
||||||
|
* // Connect API actions here.
|
||||||
|
* });
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
};
|
||||||
16
tests/test_app/src/Controller/AppController.php
Normal file
16
tests/test_app/src/Controller/AppController.php
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace TestApp\Controller;
|
||||||
|
|
||||||
|
use Cake\Controller\Controller;
|
||||||
|
|
||||||
|
class AppController extends Controller {
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function initialize(): void {
|
||||||
|
parent::initialize();
|
||||||
|
|
||||||
|
$this->loadComponent('Flash');
|
||||||
|
}
|
||||||
|
}
|
||||||
11
tests/test_app/src/View/AppView.php
Normal file
11
tests/test_app/src/View/AppView.php
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace TestApp\View;
|
||||||
|
|
||||||
|
use Cake\View\View;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @property \TinyAuth\View\Helper\AuthUserHelper $AuthUser
|
||||||
|
*/
|
||||||
|
class AppView extends View {
|
||||||
|
}
|
||||||
44
tests/test_app/templates/Error/error500.php
Normal file
44
tests/test_app/templates/Error/error500.php
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Cake\Core\Configure;
|
||||||
|
use Cake\Error\Debugger;
|
||||||
|
|
||||||
|
$this->layout = 'error';
|
||||||
|
|
||||||
|
if (Configure::read('debug')):
|
||||||
|
$this->layout = 'dev_error';
|
||||||
|
|
||||||
|
$this->assign('title', $message);
|
||||||
|
$this->assign('templateName', 'error500.ctp');
|
||||||
|
|
||||||
|
$this->start('file');
|
||||||
|
?>
|
||||||
|
<?php if (!empty($error->queryString)) : ?>
|
||||||
|
<p class="notice">
|
||||||
|
<strong>SQL Query: </strong>
|
||||||
|
<?= h($error->queryString) ?>
|
||||||
|
</p>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if (!empty($error->params)) : ?>
|
||||||
|
<strong>SQL Query Params: </strong>
|
||||||
|
<?php Debugger::dump($error->params) ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if ($error instanceof Error) : ?>
|
||||||
|
<strong>Error in: </strong>
|
||||||
|
<?= sprintf('%s, line %s', str_replace(ROOT, 'ROOT', $error->getFile()), $error->getLine()) ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php
|
||||||
|
echo $this->element('auto_table_warning');
|
||||||
|
|
||||||
|
if (extension_loaded('xdebug')):
|
||||||
|
xdebug_print_function_stack();
|
||||||
|
endif;
|
||||||
|
|
||||||
|
$this->end();
|
||||||
|
endif;
|
||||||
|
?>
|
||||||
|
<h2><?= __d('cake', 'An Internal Error Has Occurred') ?></h2>
|
||||||
|
<p class="error">
|
||||||
|
<strong><?= __d('cake', 'Error') ?>: </strong>
|
||||||
|
<?= h($message) ?>
|
||||||
|
</p>
|
||||||
6
tests/test_app/templates/layout/ajax.php
Normal file
6
tests/test_app/templates/layout/ajax.php
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* @var \App\View\AppView $this
|
||||||
|
*/
|
||||||
|
?>
|
||||||
|
<?= $this->fetch('content') ?>
|
||||||
6
tests/test_app/templates/layout/default.php
Normal file
6
tests/test_app/templates/layout/default.php
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* @var \App\View\AppView $this
|
||||||
|
*/
|
||||||
|
?>
|
||||||
|
<?= $this->fetch('content') ?>
|
||||||
BIN
tests/test_app/webroot/images/cake_icon.png
Normal file
BIN
tests/test_app/webroot/images/cake_icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 943 B |
Reference in New Issue
Block a user