Test Type classes
This commit is contained in:
parent
78a37c736f
commit
51eb460ce9
@ -2,16 +2,16 @@
|
|||||||
/**
|
/**
|
||||||
* Hummingbird Anime List Client
|
* Hummingbird Anime List Client
|
||||||
*
|
*
|
||||||
* An API client for Kitsu and MyAnimeList to manage anime and manga watch lists
|
* An API client for Kitsu to manage anime and manga watch lists
|
||||||
*
|
*
|
||||||
* PHP version 7
|
* PHP version 8
|
||||||
*
|
*
|
||||||
* @package HummingbirdAnimeClient
|
* @package HummingbirdAnimeClient
|
||||||
* @author Timothy J. Warren <tim@timshomepage.net>
|
* @author Timothy J. Warren <tim@timshomepage.net>
|
||||||
* @copyright 2015 - 2017 Timothy J. Warren
|
* @copyright 2015 - 2021 Timothy J. Warren
|
||||||
* @license http://www.opensource.org/licenses/mit-license.html MIT License
|
* @license http://www.opensource.org/licenses/mit-license.html MIT License
|
||||||
* @version 4.0
|
* @version 5.2
|
||||||
* @link https://github.com/timw4mail/HummingBirdAnimeClient
|
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
|
||||||
*/
|
*/
|
||||||
|
|
||||||
use function Aviat\AnimeClient\loadConfig;
|
use function Aviat\AnimeClient\loadConfig;
|
||||||
@ -21,12 +21,13 @@ use function Aviat\AnimeClient\loadConfig;
|
|||||||
//
|
//
|
||||||
// You shouldn't generally need to change anything below this line
|
// You shouldn't generally need to change anything below this line
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
$APP_DIR = realpath(__DIR__ . '/../');
|
$APP_DIR = dirname(__DIR__);
|
||||||
$ROOT_DIR = realpath("{$APP_DIR}/../");
|
$ROOT_DIR = dirname($APP_DIR);
|
||||||
|
|
||||||
$tomlConfig = loadConfig(__DIR__);
|
$tomlConfig = loadConfig(__DIR__);
|
||||||
|
|
||||||
return array_merge($tomlConfig, [
|
return array_merge($tomlConfig, [
|
||||||
|
'root' => $ROOT_DIR,
|
||||||
'asset_dir' => "{$ROOT_DIR}/public",
|
'asset_dir' => "{$ROOT_DIR}/public",
|
||||||
'base_config_dir' => __DIR__,
|
'base_config_dir' => __DIR__,
|
||||||
'config_dir' => "{$APP_DIR}/config",
|
'config_dir' => "{$APP_DIR}/config",
|
||||||
|
@ -2,15 +2,15 @@
|
|||||||
/**
|
/**
|
||||||
* Hummingbird Anime List Client
|
* Hummingbird Anime List Client
|
||||||
*
|
*
|
||||||
* An API client for Kitsu and MyAnimeList to manage anime and manga watch lists
|
* An API client for Kitsu to manage anime and manga watch lists
|
||||||
*
|
*
|
||||||
* PHP version 7
|
* PHP version 8
|
||||||
*
|
*
|
||||||
* @package HummingbirdAnimeClient
|
* @package HummingbirdAnimeClient
|
||||||
* @author Timothy J. Warren <tim@timshomepage.net>
|
* @author Timothy J. Warren <tim@timshomepage.net>
|
||||||
* @copyright 2015 - 2018 Timothy J. Warren
|
* @copyright 2015 - 2021 Timothy J. Warren
|
||||||
* @license http://www.opensource.org/licenses/mit-license.html MIT License
|
* @license http://www.opensource.org/licenses/mit-license.html MIT License
|
||||||
* @version 4.0
|
* @version 5.2
|
||||||
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
|
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
@ -34,11 +34,11 @@ use Psr\SimpleCache\CacheInterface;
|
|||||||
|
|
||||||
use function Aviat\Ion\_dir;
|
use function Aviat\Ion\_dir;
|
||||||
|
|
||||||
if ( ! defined('APP_DIR'))
|
if ( ! defined('HB_APP_DIR'))
|
||||||
{
|
{
|
||||||
define('APP_DIR', __DIR__);
|
define('HB_APP_DIR', __DIR__);
|
||||||
define('ROOT_DIR', dirname(APP_DIR));
|
define('ROOT_DIR', dirname(HB_APP_DIR));
|
||||||
define('TEMPLATE_DIR', _dir(APP_DIR, 'templates'));
|
define('TEMPLATE_DIR', _dir(HB_APP_DIR, 'templates'));
|
||||||
}
|
}
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
@ -50,7 +50,7 @@ return static function (array $configArray = []): Container {
|
|||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Logging
|
// Logging
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
$LOG_DIR = _dir(APP_DIR, 'logs');
|
$LOG_DIR = _dir(HB_APP_DIR, 'logs');
|
||||||
|
|
||||||
$appLogger = new Logger('animeclient');
|
$appLogger = new Logger('animeclient');
|
||||||
$appLogger->pushHandler(new RotatingFileHandler(_dir($LOG_DIR, 'app.log'), 2, Logger::WARNING));
|
$appLogger->pushHandler(new RotatingFileHandler(_dir($LOG_DIR, 'app.log'), 2, Logger::WARNING));
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
$file_patterns = [
|
$file_patterns = [
|
||||||
|
'app/appConf/*.php',
|
||||||
'app/bootstrap.php',
|
'app/bootstrap.php',
|
||||||
'migrations/*.php',
|
'migrations/*.php',
|
||||||
'src/**/*.php',
|
'src/**/*.php',
|
||||||
@ -16,7 +17,7 @@ if ( ! function_exists('glob_recursive'))
|
|||||||
{
|
{
|
||||||
// Does not support flag GLOB_BRACE
|
// Does not support flag GLOB_BRACE
|
||||||
|
|
||||||
function glob_recursive($pattern, $flags = 0)
|
function glob_recursive(string $pattern, int $flags = 0): array
|
||||||
{
|
{
|
||||||
$files = glob($pattern, $flags);
|
$files = glob($pattern, $flags);
|
||||||
|
|
||||||
@ -57,17 +58,21 @@ function get_text_to_replace(array $tokens): string
|
|||||||
return $output;
|
return $output;
|
||||||
}
|
}
|
||||||
|
|
||||||
function get_tokens($source): array
|
function get_tokens(string $source): array
|
||||||
{
|
{
|
||||||
return token_get_all($source);
|
return token_get_all($source);
|
||||||
}
|
}
|
||||||
|
|
||||||
function replace_files(array $files, $template)
|
function replace_files(array $files, string $template): void
|
||||||
{
|
{
|
||||||
print_r($files);
|
print_r($files);
|
||||||
foreach ($files as $file)
|
foreach ($files as $file)
|
||||||
{
|
{
|
||||||
$source = file_get_contents($file);
|
$source = file_get_contents($file);
|
||||||
|
if ($source === FALSE)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (stripos($source, 'namespace') === FALSE)
|
if (stripos($source, 'namespace') === FALSE)
|
||||||
{
|
{
|
||||||
|
@ -4,6 +4,7 @@ parameters:
|
|||||||
inferPrivatePropertyTypeFromConstructor: true
|
inferPrivatePropertyTypeFromConstructor: true
|
||||||
level: 8
|
level: 8
|
||||||
paths:
|
paths:
|
||||||
|
- app/appConf
|
||||||
- src
|
- src
|
||||||
- ./console
|
- ./console
|
||||||
- index.php
|
- index.php
|
||||||
|
@ -186,7 +186,7 @@ function checkFolderPermissions(ConfigInterface $config): array
|
|||||||
$errors = [];
|
$errors = [];
|
||||||
$publicDir = $config->get('asset_dir');
|
$publicDir = $config->get('asset_dir');
|
||||||
|
|
||||||
$APP_DIR = _dir(dirname(__DIR__, 2), '/app');
|
$APP_DIR = _dir($config->get('root'), 'app');
|
||||||
|
|
||||||
$pathMap = [
|
$pathMap = [
|
||||||
'app/config' => "{$APP_DIR}/config",
|
'app/config' => "{$APP_DIR}/config",
|
||||||
@ -211,7 +211,9 @@ function checkFolderPermissions(ConfigInterface $config): array
|
|||||||
|
|
||||||
if ( ! $writable)
|
if ( ! $writable)
|
||||||
{
|
{
|
||||||
|
// @codeCoverageIgnoreStart
|
||||||
$errors['writable'][] = $pretty;
|
$errors['writable'][] = $pretty;
|
||||||
|
// @codeCoverageIgnoreEnd
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -292,6 +294,7 @@ function getLocalImg (string $kitsuUrl, $webp = TRUE): string
|
|||||||
/**
|
/**
|
||||||
* Create a transparent placeholder image
|
* Create a transparent placeholder image
|
||||||
*
|
*
|
||||||
|
* @codeCoverageIgnore
|
||||||
* @param string $path
|
* @param string $path
|
||||||
* @param int|null $width
|
* @param int|null $width
|
||||||
* @param int|null $height
|
* @param int|null $height
|
||||||
@ -378,7 +381,6 @@ function colNotEmpty(array $search, string $key): bool
|
|||||||
*
|
*
|
||||||
* @param CacheInterface $cache
|
* @param CacheInterface $cache
|
||||||
* @return bool
|
* @return bool
|
||||||
* @throws Throwable
|
|
||||||
*/
|
*/
|
||||||
function clearCache(CacheInterface $cache): bool
|
function clearCache(CacheInterface $cache): bool
|
||||||
{
|
{
|
||||||
@ -393,9 +395,7 @@ function clearCache(CacheInterface $cache): bool
|
|||||||
$userData = array_filter((array)$userData, static fn ($value) => $value !== NULL);
|
$userData = array_filter((array)$userData, static fn ($value) => $value !== NULL);
|
||||||
$cleared = $cache->clear();
|
$cleared = $cache->clear();
|
||||||
|
|
||||||
$saved = ( ! empty($userData))
|
$saved = ( ! empty($userData)) ? $cache->setMultiple($userData) : TRUE;
|
||||||
? $cache->setMultiple($userData)
|
|
||||||
: TRUE;
|
|
||||||
|
|
||||||
return $cleared && $saved;
|
return $cleared && $saved;
|
||||||
}
|
}
|
||||||
|
@ -236,6 +236,9 @@ abstract class AbstractType implements ArrayAccess, Countable {
|
|||||||
return TRUE;
|
return TRUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @codeCoverageIgnore
|
||||||
|
*/
|
||||||
final protected function fromObject(mixed $parent = null): float|null|bool|int|array|string
|
final protected function fromObject(mixed $parent = null): float|null|bool|int|array|string
|
||||||
{
|
{
|
||||||
$object = $parent ?? $this;
|
$object = $parent ?? $this;
|
||||||
|
@ -32,6 +32,8 @@ class Config extends AbstractType {
|
|||||||
// Settings in config.toml
|
// Settings in config.toml
|
||||||
// ------------------------------------------------------------------------
|
// ------------------------------------------------------------------------
|
||||||
|
|
||||||
|
public string $root; // Path to app root
|
||||||
|
|
||||||
public ?string $asset_path; // Path to public folder for urls
|
public ?string $asset_path; // Path to public folder for urls
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -62,8 +64,6 @@ class Config extends AbstractType {
|
|||||||
/**
|
/**
|
||||||
* Default list view type
|
* Default list view type
|
||||||
* 'cover_view' or 'list_view'
|
* 'cover_view' or 'list_view'
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
*/
|
||||||
public ?string $default_view_type;
|
public ?string $default_view_type;
|
||||||
|
|
||||||
@ -71,21 +71,13 @@ class Config extends AbstractType {
|
|||||||
|
|
||||||
public bool $secure_urls = TRUE;
|
public bool $secure_urls = TRUE;
|
||||||
|
|
||||||
/**
|
|
||||||
* @var string|bool
|
|
||||||
*/
|
|
||||||
public string|bool $show_anime_collection = FALSE;
|
public string|bool $show_anime_collection = FALSE;
|
||||||
|
|
||||||
/**
|
|
||||||
* @var string|bool
|
|
||||||
*/
|
|
||||||
public string|bool $show_manga_collection = FALSE;
|
public string|bool $show_manga_collection = FALSE;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CSS theme: light, dark, or auto-switching
|
* CSS theme: light, dark, or auto-switching
|
||||||
* 'auto', 'light', or 'dark'
|
* 'auto', 'light', or 'dark'
|
||||||
*
|
|
||||||
* @var string|null
|
|
||||||
*/
|
*/
|
||||||
public ?string $theme = 'auto';
|
public ?string $theme = 'auto';
|
||||||
|
|
||||||
|
@ -16,9 +16,11 @@
|
|||||||
|
|
||||||
namespace Aviat\AnimeClient\Tests;
|
namespace Aviat\AnimeClient\Tests;
|
||||||
|
|
||||||
use Amp\Http\Client\Response;
|
|
||||||
|
|
||||||
use function Aviat\AnimeClient\arrayToToml;
|
use function Aviat\AnimeClient\arrayToToml;
|
||||||
|
use function Aviat\AnimeClient\checkFolderPermissions;
|
||||||
|
use function Aviat\AnimeClient\clearCache;
|
||||||
|
use function Aviat\AnimeClient\colNotEmpty;
|
||||||
|
use function Aviat\AnimeClient\getLocalImg;
|
||||||
use function Aviat\AnimeClient\getResponse;
|
use function Aviat\AnimeClient\getResponse;
|
||||||
use function Aviat\AnimeClient\isSequentialArray;
|
use function Aviat\AnimeClient\isSequentialArray;
|
||||||
use function Aviat\AnimeClient\tomlToArray;
|
use function Aviat\AnimeClient\tomlToArray;
|
||||||
@ -89,4 +91,46 @@ class AnimeClientTest extends AnimeClientTestCase
|
|||||||
{
|
{
|
||||||
$this->assertNotEmpty(getResponse('https://example.com'));
|
$this->assertNotEmpty(getResponse('https://example.com'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testCheckFolderPermissions(): void
|
||||||
|
{
|
||||||
|
$config = $this->container->get('config');
|
||||||
|
$actual = checkFolderPermissions($config);
|
||||||
|
$this->assertTrue(is_array($actual));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetLocalImageEmptyUrl(): void
|
||||||
|
{
|
||||||
|
$actual = getLocalImg('');
|
||||||
|
$this->assertEquals('images/placeholder.webp', $actual);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetLocalImageBadUrl(): void
|
||||||
|
{
|
||||||
|
$actual = getLocalImg('//foo.bar');
|
||||||
|
$this->assertEquals('images/placeholder.webp', $actual);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testColNotEmpty(): void
|
||||||
|
{
|
||||||
|
$hasEmptyCols = [[
|
||||||
|
'foo' => '',
|
||||||
|
], [
|
||||||
|
'foo' => '',
|
||||||
|
]];
|
||||||
|
|
||||||
|
$hasNonEmptyCols = [[
|
||||||
|
'foo' => 'bar',
|
||||||
|
], [
|
||||||
|
'foo' => 'baz',
|
||||||
|
]];
|
||||||
|
|
||||||
|
$this->assertEquals(false, colNotEmpty($hasEmptyCols, 'foo'));
|
||||||
|
$this->assertEquals(true, colNotEmpty($hasNonEmptyCols, 'foo'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testClearCache(): void
|
||||||
|
{
|
||||||
|
$this->assertTrue(clearCache($this->container->get('cache')));
|
||||||
|
}
|
||||||
}
|
}
|
@ -16,6 +16,7 @@
|
|||||||
|
|
||||||
namespace Aviat\AnimeClient\Tests;
|
namespace Aviat\AnimeClient\Tests;
|
||||||
|
|
||||||
|
use Aviat\Ion\Di\ContainerInterface;
|
||||||
use function Aviat\Ion\_dir;
|
use function Aviat\Ion\_dir;
|
||||||
|
|
||||||
use Aviat\Ion\Json;
|
use Aviat\Ion\Json;
|
||||||
@ -59,6 +60,7 @@ class AnimeClientTestCase extends TestCase {
|
|||||||
parent::setUp();
|
parent::setUp();
|
||||||
|
|
||||||
$config_array = [
|
$config_array = [
|
||||||
|
'root' => self::ROOT_DIR,
|
||||||
'asset_path' => '/assets',
|
'asset_path' => '/assets',
|
||||||
'img_cache_path' => _dir(self::ROOT_DIR, 'public/images'),
|
'img_cache_path' => _dir(self::ROOT_DIR, 'public/images'),
|
||||||
'data_cache_path' => _dir(self::TEST_DATA_DIR, 'cache'),
|
'data_cache_path' => _dir(self::TEST_DATA_DIR, 'cache'),
|
||||||
@ -94,7 +96,7 @@ class AnimeClientTestCase extends TestCase {
|
|||||||
];
|
];
|
||||||
|
|
||||||
// Set up DI container
|
// Set up DI container
|
||||||
$di = require _dir(self::ROOT_DIR, 'app', 'bootstrap.php');
|
$di = require self::ROOT_DIR . '/app/bootstrap.php';
|
||||||
$container = $di($config_array);
|
$container = $di($config_array);
|
||||||
|
|
||||||
// Use mock session handler
|
// Use mock session handler
|
||||||
|
53
tests/AnimeClient/Types/ConfigTest.php
Normal file
53
tests/AnimeClient/Types/ConfigTest.php
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
/**
|
||||||
|
* Hummingbird Anime List Client
|
||||||
|
*
|
||||||
|
* An API client for Kitsu to manage anime and manga watch lists
|
||||||
|
*
|
||||||
|
* PHP version 8
|
||||||
|
*
|
||||||
|
* @package HummingbirdAnimeClient
|
||||||
|
* @author Timothy J. Warren <tim@timshomepage.net>
|
||||||
|
* @copyright 2015 - 2021 Timothy J. Warren
|
||||||
|
* @license http://www.opensource.org/licenses/mit-license.html MIT License
|
||||||
|
* @version 5.2
|
||||||
|
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Aviat\AnimeClient\Tests\Types;
|
||||||
|
|
||||||
|
use Aviat\AnimeClient\Types\Config;
|
||||||
|
use Aviat\AnimeClient\Types\UndefinedPropertyException;
|
||||||
|
|
||||||
|
class ConfigTest extends ConfigTestCase {
|
||||||
|
public function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
$this->testClass = Config::class;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSetMethods(): void
|
||||||
|
{
|
||||||
|
$type = $this->testClass::from([
|
||||||
|
'anilist' => [],
|
||||||
|
'cache' => [],
|
||||||
|
'database' => [],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertEquals(3, $type->count());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testOffsetUnset(): void
|
||||||
|
{
|
||||||
|
$type = $this->testClass::from([
|
||||||
|
'anilist' => [],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertTrue($type->offsetExists('anilist'));
|
||||||
|
|
||||||
|
$type->offsetUnset('anilist');
|
||||||
|
|
||||||
|
$this->assertNotTrue($type->offsetExists('anilist'));
|
||||||
|
}
|
||||||
|
}
|
72
tests/AnimeClient/Types/ConfigTestCase.php
Normal file
72
tests/AnimeClient/Types/ConfigTestCase.php
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
/**
|
||||||
|
* Hummingbird Anime List Client
|
||||||
|
*
|
||||||
|
* An API client for Kitsu to manage anime and manga watch lists
|
||||||
|
*
|
||||||
|
* PHP version 8
|
||||||
|
*
|
||||||
|
* @package HummingbirdAnimeClient
|
||||||
|
* @author Timothy J. Warren <tim@timshomepage.net>
|
||||||
|
* @copyright 2015 - 2021 Timothy J. Warren
|
||||||
|
* @license http://www.opensource.org/licenses/mit-license.html MIT License
|
||||||
|
* @version 5.2
|
||||||
|
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Aviat\AnimeClient\Tests\Types;
|
||||||
|
|
||||||
|
use Aviat\AnimeClient\Tests\AnimeClientTestCase;
|
||||||
|
use Aviat\AnimeClient\Types\UndefinedPropertyException;
|
||||||
|
|
||||||
|
abstract class ConfigTestCase extends AnimeClientTestCase {
|
||||||
|
public string $testClass;
|
||||||
|
|
||||||
|
public function testCheck(): void
|
||||||
|
{
|
||||||
|
$result = $this->testClass::check([]);
|
||||||
|
$this->assertEquals([], $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSetUndefinedProperty(): void
|
||||||
|
{
|
||||||
|
$this->expectException(UndefinedPropertyException::class);
|
||||||
|
$this->testClass::from([
|
||||||
|
'foobar' => 'baz',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testToString(): void
|
||||||
|
{
|
||||||
|
$actual = $this->testClass::from([])->__toString();
|
||||||
|
$this->assertMatchesSnapshot($actual);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testOffsetExists(): void
|
||||||
|
{
|
||||||
|
$actual = $this->testClass::from([
|
||||||
|
'anilist' => [],
|
||||||
|
])->offsetExists('anilist');
|
||||||
|
$this->assertTrue($actual);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSetState(): void
|
||||||
|
{
|
||||||
|
$normal = $this->testClass::from([]);
|
||||||
|
$setState = $this->testClass::__set_state([]);
|
||||||
|
|
||||||
|
$this->assertEquals($normal, $setState);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testIsEmpty(): void
|
||||||
|
{
|
||||||
|
$type = $this->testClass::from([]);
|
||||||
|
$this->assertTrue($type->isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testCount(): void
|
||||||
|
{
|
||||||
|
$type = $this->testClass::from([]);
|
||||||
|
$this->assertEquals(0, $type->count());
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,3 @@
|
|||||||
|
Aviat\AnimeClient\Types\Config Object
|
||||||
|
(
|
||||||
|
)
|
Loading…
Reference in New Issue
Block a user