feat: add comprehensive unit testing support for CakePHP
- Add 4 CakePHP Claude Code skills: cakephp-unit-test, cakephp-test-setup, cakephp-test-review, cakephp-mock-helper - Add 11 CakePHP PHPUnit component templates (Table, Entity, Controller, Service, Command, Component, Middleware, Mailer, Form, Behavior, Helper) - Add CakePHP configs (phpunit.xml.dist, app_local.test.php for SQLite in-memory) - Add comprehensive CakePHP documentation and component guides - Update test-runner.sh and installer to auto-detect Laravel and CakePHP - Add CakePHP CI/CD templates for GitHub Actions and GitLab CI - Update system prompt, verification checklist, and README with dedicated guides
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
/*
|
||||
* Local test configuration for CakePHP
|
||||
* Đảm bảo Datasources.test dùng SQLite In-memory (:memory:) an toàn, tốc độ cao.
|
||||
*/
|
||||
return [
|
||||
'Datasources' => [
|
||||
'test' => [
|
||||
'className' => 'Cake\Database\Connection',
|
||||
'driver' => 'Cake\Database\Driver\Sqlite',
|
||||
'persistent' => false,
|
||||
'timezone' => 'UTC',
|
||||
'flags' => [],
|
||||
'encoding' => 'utf8',
|
||||
'database' => ':memory:',
|
||||
'init' => [],
|
||||
'cacheMetadata' => true,
|
||||
'quoteIdentifiers' => false,
|
||||
'log' => false,
|
||||
],
|
||||
],
|
||||
];
|
||||
@@ -0,0 +1,37 @@
|
||||
<?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.2/phpunit.xsd"
|
||||
cacheDirectory=".phpunit.cache">
|
||||
<php>
|
||||
<ini name="memory_limit" value="-1"/>
|
||||
<ini name="apc.enable_cli" value="1"/>
|
||||
<env name="FIXTURE_SCHEMA_METADATA" value="tests/schema.php"/>
|
||||
</php>
|
||||
|
||||
<!-- Add any additional test suites you want to run here -->
|
||||
<testsuites>
|
||||
<testsuite name="app">
|
||||
<directory>tests/TestCase/</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
|
||||
<!-- Setup a listener for fixtures -->
|
||||
<extensions>
|
||||
<bootstrap class="Cake\TestSuite\Fixture\Extension\PHPUnitExtension"/>
|
||||
</extensions>
|
||||
|
||||
<source>
|
||||
<include>
|
||||
<directory suffix=".php">src/</directory>
|
||||
<directory suffix=".php">plugins/*/src/</directory>
|
||||
</include>
|
||||
<exclude>
|
||||
<file>src/Console/Installer.php</file>
|
||||
</exclude>
|
||||
</source>
|
||||
</phpunit>
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Test\TestCase\Model\Behavior;
|
||||
|
||||
use App\Model\Behavior\{{BEHAVIOR_NAME}}Behavior;
|
||||
use Cake\ORM\Table;
|
||||
use Cake\TestSuite\TestCase;
|
||||
|
||||
/**
|
||||
* App\Model\Behavior\{{BEHAVIOR_NAME}}Behavior Test Case
|
||||
*/
|
||||
class {{BEHAVIOR_NAME}}BehaviorTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* Test subject
|
||||
*
|
||||
* @var \App\Model\Behavior\{{BEHAVIOR_NAME}}Behavior
|
||||
*/
|
||||
protected {{BEHAVIOR_NAME}}Behavior ${{VARIABLE_NAME}};
|
||||
|
||||
/**
|
||||
* Table instance
|
||||
*
|
||||
* @var \Cake\ORM\Table
|
||||
*/
|
||||
protected Table $table;
|
||||
|
||||
/**
|
||||
* setUp method
|
||||
*/
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->table = new Table(['table' => 'test_table']);
|
||||
$this->{{VARIABLE_NAME}} = new {{BEHAVIOR_NAME}}Behavior($this->table);
|
||||
}
|
||||
|
||||
/**
|
||||
* tearDown method
|
||||
*/
|
||||
protected function tearDown(): void
|
||||
{
|
||||
unset($this->{{VARIABLE_NAME}}, $this->table);
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test behavior initialization
|
||||
*/
|
||||
public function testInitialization(): void
|
||||
{
|
||||
$this->assertInstanceOf({{BEHAVIOR_NAME}}Behavior::class, $this->{{VARIABLE_NAME}});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Test\TestCase\Command;
|
||||
|
||||
use Cake\Console\TestSuite\ConsoleIntegrationTestTrait;
|
||||
use Cake\TestSuite\TestCase;
|
||||
|
||||
/**
|
||||
* App\Command\{{COMMAND_NAME}}Command Test Case
|
||||
*/
|
||||
class {{COMMAND_NAME}}CommandTest extends TestCase
|
||||
{
|
||||
use ConsoleIntegrationTestTrait;
|
||||
|
||||
/**
|
||||
* setUp method
|
||||
*/
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->useCommandRunner();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test execute with success
|
||||
*/
|
||||
public function testExecuteSuccess(): void
|
||||
{
|
||||
// Act
|
||||
$this->exec('{{COMMAND_SLUG}}');
|
||||
|
||||
// Assert
|
||||
$this->assertExitSuccess();
|
||||
$this->assertOutputContains('Done');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test execute with invalid argument
|
||||
*/
|
||||
public function testExecuteFailsOnMissingArgument(): void
|
||||
{
|
||||
// Act
|
||||
$this->exec('{{COMMAND_SLUG}} --invalid-flag');
|
||||
|
||||
// Assert
|
||||
$this->assertExitError();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Test\TestCase\Controller\Component;
|
||||
|
||||
use App\Controller\Component\{{COMPONENT_NAME}}Component;
|
||||
use Cake\Controller\ComponentRegistry;
|
||||
use Cake\Controller\Controller;
|
||||
use Cake\Http\ServerRequest;
|
||||
use Cake\TestSuite\TestCase;
|
||||
|
||||
/**
|
||||
* App\Controller\Component\{{COMPONENT_NAME}}Component Test Case
|
||||
*/
|
||||
class {{COMPONENT_NAME}}ComponentTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* Test subject
|
||||
*
|
||||
* @var \App\Controller\Component\{{COMPONENT_NAME}}Component
|
||||
*/
|
||||
protected {{COMPONENT_NAME}}Component ${{VARIABLE_NAME}};
|
||||
|
||||
/**
|
||||
* setUp method
|
||||
*/
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$controller = new Controller(new ServerRequest());
|
||||
$registry = new ComponentRegistry($controller);
|
||||
$this->{{VARIABLE_NAME}} = new {{COMPONENT_NAME}}Component($registry);
|
||||
}
|
||||
|
||||
/**
|
||||
* tearDown method
|
||||
*/
|
||||
protected function tearDown(): void
|
||||
{
|
||||
unset($this->{{VARIABLE_NAME}});
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test basic method
|
||||
*/
|
||||
public function testDoActionSuccess(): void
|
||||
{
|
||||
// Act
|
||||
$result = $this->{{VARIABLE_NAME}}->doAction();
|
||||
|
||||
// Assert
|
||||
$this->assertTrue($result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Test\TestCase\Controller;
|
||||
|
||||
use Cake\TestSuite\IntegrationTestTrait;
|
||||
use Cake\TestSuite\TestCase;
|
||||
|
||||
/**
|
||||
* App\Controller\{{CONTROLLER_NAME}}Controller Test Case
|
||||
*
|
||||
* @uses \App\Controller\{{CONTROLLER_NAME}}Controller
|
||||
*/
|
||||
class {{CONTROLLER_NAME}}ControllerTest extends TestCase
|
||||
{
|
||||
use IntegrationTestTrait;
|
||||
|
||||
/**
|
||||
* Fixtures
|
||||
*
|
||||
* @var list<string>
|
||||
*/
|
||||
protected array $fixtures = [
|
||||
'app.{{MODEL_NAME}}',
|
||||
];
|
||||
|
||||
/**
|
||||
* Test index method
|
||||
*/
|
||||
public function testIndex(): void
|
||||
{
|
||||
// Act
|
||||
$this->get('/{{ROUTE_PATH}}');
|
||||
|
||||
// Assert
|
||||
$this->assertResponseOk();
|
||||
$this->assertResponseContains('{{EXPECTED_TEXT}}');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test view method with valid ID
|
||||
*/
|
||||
public function testViewValidId(): void
|
||||
{
|
||||
// Act
|
||||
$this->get('/{{ROUTE_PATH}}/view/1');
|
||||
|
||||
// Assert
|
||||
$this->assertResponseOk();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test add method GET renders form
|
||||
*/
|
||||
public function testAddGet(): void
|
||||
{
|
||||
// Act
|
||||
$this->get('/{{ROUTE_PATH}}/add');
|
||||
|
||||
// Assert
|
||||
$this->assertResponseOk();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test add method POST with valid data
|
||||
*/
|
||||
public function testAddPostSuccess(): void
|
||||
{
|
||||
// Arrange
|
||||
$this->enableCsrfToken();
|
||||
$this->enableSecurityToken();
|
||||
|
||||
$data = [
|
||||
'name' => 'New Item',
|
||||
'status' => 'active',
|
||||
];
|
||||
|
||||
// Act
|
||||
$this->post('/{{ROUTE_PATH}}/add', $data);
|
||||
|
||||
// Assert
|
||||
$this->assertResponseSuccess();
|
||||
$this->assertRedirect(['action' => 'index']);
|
||||
$this->assertFlashMessage('The {{ENTITY_LOWER}} has been saved.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test add method POST with validation errors
|
||||
*/
|
||||
public function testAddPostValidationFailure(): void
|
||||
{
|
||||
// Arrange
|
||||
$this->enableCsrfToken();
|
||||
$this->enableSecurityToken();
|
||||
|
||||
$data = []; // Empty or invalid data
|
||||
|
||||
// Act
|
||||
$this->post('/{{ROUTE_PATH}}/add', $data);
|
||||
|
||||
// Assert
|
||||
$this->assertResponseOk(); // Renders form view with errors
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Test\TestCase\Model\Entity;
|
||||
|
||||
use App\Model\Entity\{{ENTITY_NAME}};
|
||||
use Cake\TestSuite\TestCase;
|
||||
|
||||
/**
|
||||
* App\Model\Entity\{{ENTITY_NAME}} Test Case
|
||||
*/
|
||||
class {{ENTITY_NAME}}Test extends TestCase
|
||||
{
|
||||
/**
|
||||
* Test subject
|
||||
*
|
||||
* @var \App\Model\Entity\{{ENTITY_NAME}}
|
||||
*/
|
||||
protected {{ENTITY_NAME}} ${{VARIABLE_NAME}};
|
||||
|
||||
/**
|
||||
* setUp method
|
||||
*/
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->{{VARIABLE_NAME}} = new {{ENTITY_NAME}}();
|
||||
}
|
||||
|
||||
/**
|
||||
* tearDown method
|
||||
*/
|
||||
protected function tearDown(): void
|
||||
{
|
||||
unset($this->{{VARIABLE_NAME}});
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test accessible fields configuration
|
||||
*/
|
||||
public function testAccessibleFields(): void
|
||||
{
|
||||
// Arrange
|
||||
$data = [
|
||||
'name' => 'John Doe',
|
||||
'email' => 'john@example.com',
|
||||
'id' => 999, // Typically non-accessible or guarded
|
||||
];
|
||||
|
||||
// Act
|
||||
$this->{{VARIABLE_NAME}}->set($data);
|
||||
|
||||
// Assert
|
||||
$this->assertSame('John Doe', $this->{{VARIABLE_NAME}}->name);
|
||||
$this->assertSame('john@example.com', $this->{{VARIABLE_NAME}}->email);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test virtual fields if present
|
||||
*/
|
||||
public function testVirtualProperties(): void
|
||||
{
|
||||
// Arrange
|
||||
$this->{{VARIABLE_NAME}}->set([
|
||||
'first_name' => 'John',
|
||||
'last_name' => 'Doe',
|
||||
]);
|
||||
|
||||
// Act & Assert
|
||||
if ($this->{{VARIABLE_NAME}}->has('full_name')) {
|
||||
$this->assertSame('John Doe', $this->{{VARIABLE_NAME}}->full_name);
|
||||
} else {
|
||||
$this->assertTrue(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Test\TestCase\Form;
|
||||
|
||||
use App\Form\{{FORM_NAME}}Form;
|
||||
use Cake\TestSuite\TestCase;
|
||||
|
||||
/**
|
||||
* App\Form\{{FORM_NAME}}Form Test Case
|
||||
*/
|
||||
class {{FORM_NAME}}FormTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* Test subject
|
||||
*
|
||||
* @var \App\Form\{{FORM_NAME}}Form
|
||||
*/
|
||||
protected {{FORM_NAME}}Form ${{VARIABLE_NAME}};
|
||||
|
||||
/**
|
||||
* setUp method
|
||||
*/
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->{{VARIABLE_NAME}} = new {{FORM_NAME}}Form();
|
||||
}
|
||||
|
||||
/**
|
||||
* tearDown method
|
||||
*/
|
||||
protected function tearDown(): void
|
||||
{
|
||||
unset($this->{{VARIABLE_NAME}});
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test validationDefault with valid data
|
||||
*/
|
||||
public function testValidationSuccess(): void
|
||||
{
|
||||
// Arrange
|
||||
$data = [
|
||||
'email' => 'user@example.com',
|
||||
'message' => 'Hello World',
|
||||
];
|
||||
|
||||
// Act
|
||||
$isValid = $this->{{VARIABLE_NAME}}->validate($data);
|
||||
|
||||
// Assert
|
||||
$this->assertTrue($isValid);
|
||||
$this->assertEmpty($this->{{VARIABLE_NAME}}->getErrors());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test execute method
|
||||
*/
|
||||
public function testExecuteSuccess(): void
|
||||
{
|
||||
// Arrange
|
||||
$data = [
|
||||
'email' => 'user@example.com',
|
||||
'message' => 'Hello World',
|
||||
];
|
||||
|
||||
// Act
|
||||
$result = $this->{{VARIABLE_NAME}}->execute($data);
|
||||
|
||||
// Assert
|
||||
$this->assertTrue($result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Test\TestCase\View\Helper;
|
||||
|
||||
use App\View\Helper\{{HELPER_NAME}}Helper;
|
||||
use Cake\TestSuite\TestCase;
|
||||
use Cake\View\View;
|
||||
|
||||
/**
|
||||
* App\View\Helper\{{HELPER_NAME}}Helper Test Case
|
||||
*/
|
||||
class {{HELPER_NAME}}HelperTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* Test subject
|
||||
*
|
||||
* @var \App\View\Helper\{{HELPER_NAME}}Helper
|
||||
*/
|
||||
protected {{HELPER_NAME}}Helper ${{VARIABLE_NAME}};
|
||||
|
||||
/**
|
||||
* setUp method
|
||||
*/
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$view = new View();
|
||||
$this->{{VARIABLE_NAME}} = new {{HELPER_NAME}}Helper($view);
|
||||
}
|
||||
|
||||
/**
|
||||
* tearDown method
|
||||
*/
|
||||
protected function tearDown(): void
|
||||
{
|
||||
unset($this->{{VARIABLE_NAME}});
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test helper initialization and rendering
|
||||
*/
|
||||
public function testInitialization(): void
|
||||
{
|
||||
$this->assertInstanceOf({{HELPER_NAME}}Helper::class, $this->{{VARIABLE_NAME}});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Test\TestCase\Mailer;
|
||||
|
||||
use App\Mailer\{{MAILER_NAME}}Mailer;
|
||||
use Cake\TestSuite\EmailTrait;
|
||||
use Cake\TestSuite\TestCase;
|
||||
|
||||
/**
|
||||
* App\Mailer\{{MAILER_NAME}}Mailer Test Case
|
||||
*/
|
||||
class {{MAILER_NAME}}MailerTest extends TestCase
|
||||
{
|
||||
use EmailTrait;
|
||||
|
||||
/**
|
||||
* Test subject
|
||||
*
|
||||
* @var \App\Mailer\{{MAILER_NAME}}Mailer
|
||||
*/
|
||||
protected {{MAILER_NAME}}Mailer ${{VARIABLE_NAME}};
|
||||
|
||||
/**
|
||||
* setUp method
|
||||
*/
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->{{VARIABLE_NAME}} = new {{MAILER_NAME}}Mailer();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test send welcome email
|
||||
*/
|
||||
public function testSendWelcomeEmail(): void
|
||||
{
|
||||
// Act
|
||||
$this->{{VARIABLE_NAME}}->send('welcome', ['user@example.com', 'John Doe']);
|
||||
|
||||
// Assert
|
||||
$this->assertMailSentTo('user@example.com');
|
||||
$this->assertMailContainsText('John Doe');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Test\TestCase\Middleware;
|
||||
|
||||
use App\Middleware\{{MIDDLEWARE_NAME}}Middleware;
|
||||
use Cake\Http\Response;
|
||||
use Cake\Http\ServerRequest;
|
||||
use Cake\TestSuite\TestCase;
|
||||
use TestApp\Http\TestRequestHandler;
|
||||
|
||||
/**
|
||||
* App\Middleware\{{MIDDLEWARE_NAME}}Middleware Test Case
|
||||
*/
|
||||
class {{MIDDLEWARE_NAME}}MiddlewareTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* Test process method
|
||||
*/
|
||||
public function testProcess(): void
|
||||
{
|
||||
// Arrange
|
||||
$middleware = new {{MIDDLEWARE_NAME}}Middleware();
|
||||
$request = new ServerRequest(['url' => '/test-path']);
|
||||
|
||||
$handler = new class implements \Psr\Http\Server\RequestHandlerInterface {
|
||||
public function handle(\Psr\Http\Message\ServerRequestInterface $request): \Psr\Http\Message\ResponseInterface
|
||||
{
|
||||
return new Response();
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
$response = $middleware->process($request, $handler);
|
||||
|
||||
// Assert
|
||||
$this->assertInstanceOf(\Psr\Http\Message\ResponseInterface::class, $response);
|
||||
$this->assertSame(200, $response->getStatusCode());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Test\TestCase\Service;
|
||||
|
||||
use App\Service\{{SERVICE_NAME}}Service;
|
||||
use Cake\TestSuite\TestCase;
|
||||
|
||||
/**
|
||||
* App\Service\{{SERVICE_NAME}}Service Test Case
|
||||
*/
|
||||
class {{SERVICE_NAME}}ServiceTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* Test subject
|
||||
*
|
||||
* @var \App\Service\{{SERVICE_NAME}}Service
|
||||
*/
|
||||
protected {{SERVICE_NAME}}Service ${{VARIABLE_NAME}};
|
||||
|
||||
/**
|
||||
* setUp method
|
||||
*/
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
// Arrange dependencies
|
||||
$this->{{VARIABLE_NAME}} = new {{SERVICE_NAME}}Service();
|
||||
}
|
||||
|
||||
/**
|
||||
* tearDown method
|
||||
*/
|
||||
protected function tearDown(): void
|
||||
{
|
||||
unset($this->{{VARIABLE_NAME}});
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test execution success (Happy Path)
|
||||
*/
|
||||
public function testExecuteSuccess(): void
|
||||
{
|
||||
// Arrange
|
||||
$input = 'sample_input';
|
||||
|
||||
// Act
|
||||
$result = $this->{{VARIABLE_NAME}}->execute($input);
|
||||
|
||||
// Assert
|
||||
$this->assertNotNull($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test execution with invalid input (Exception Path)
|
||||
*/
|
||||
public function testExecuteThrowsExceptionOnInvalidInput(): void
|
||||
{
|
||||
// Assert
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
|
||||
// Act
|
||||
$this->{{VARIABLE_NAME}}->execute('');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Test\TestCase\Model\Table;
|
||||
|
||||
use App\Model\Table\{{TABLE_NAME}}Table;
|
||||
use Cake\TestSuite\TestCase;
|
||||
|
||||
/**
|
||||
* App\Model\Table\{{TABLE_NAME}}Table Test Case
|
||||
*/
|
||||
class {{TABLE_NAME}}TableTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* Test subject
|
||||
*
|
||||
* @var \App\Model\Table\{{TABLE_NAME}}Table
|
||||
*/
|
||||
protected {{TABLE_NAME}}Table ${{VARIABLE_NAME}};
|
||||
|
||||
/**
|
||||
* Fixtures
|
||||
*
|
||||
* @var list<string>
|
||||
*/
|
||||
protected array $fixtures = [
|
||||
'app.{{TABLE_NAME}}',
|
||||
];
|
||||
|
||||
/**
|
||||
* setUp method
|
||||
*/
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$config = $this->getTableLocator()->exists('{{TABLE_NAME}}') ? [] : ['className' => {{TABLE_NAME}}Table::class];
|
||||
$this->{{VARIABLE_NAME}} = $this->getTableLocator()->get('{{TABLE_NAME}}', $config);
|
||||
}
|
||||
|
||||
/**
|
||||
* tearDown method
|
||||
*/
|
||||
protected function tearDown(): void
|
||||
{
|
||||
unset($this->{{VARIABLE_NAME}});
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test validationDefault method with valid data
|
||||
*/
|
||||
public function testValidationDefaultSuccess(): void
|
||||
{
|
||||
// Arrange
|
||||
$data = [
|
||||
'name' => 'Valid Title',
|
||||
'status' => 'active',
|
||||
];
|
||||
|
||||
// Act
|
||||
$entity = $this->{{VARIABLE_NAME}}->newEntity($data);
|
||||
|
||||
// Assert
|
||||
$this->assertEmpty($entity->getErrors());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test validationDefault method with missing required fields
|
||||
*/
|
||||
public function testValidationDefaultFailsOnRequiredFields(): void
|
||||
{
|
||||
// Arrange
|
||||
$data = [];
|
||||
|
||||
// Act
|
||||
$entity = $this->{{VARIABLE_NAME}}->newEntity($data);
|
||||
|
||||
// Assert
|
||||
$this->assertNotEmpty($entity->getErrors());
|
||||
$this->assertArrayHasKey('name', $entity->getErrors());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test save and find
|
||||
*/
|
||||
public function testSaveAndFind(): void
|
||||
{
|
||||
// Arrange
|
||||
$entity = $this->{{VARIABLE_NAME}}->newEntity([
|
||||
'name' => 'Sample Record',
|
||||
'status' => 'active',
|
||||
]);
|
||||
|
||||
// Act
|
||||
$result = $this->{{VARIABLE_NAME}}->save($entity);
|
||||
|
||||
// Assert
|
||||
$this->assertNotFalse($result);
|
||||
$this->assertNotNull($result->id);
|
||||
|
||||
$saved = $this->{{VARIABLE_NAME}}->get($result->id);
|
||||
$this->assertSame('Sample Record', $saved->name);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
name: CakePHP Unit Tests & Quality Gate
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, master, develop ]
|
||||
pull_request:
|
||||
branches: [ main, master, develop ]
|
||||
|
||||
jobs:
|
||||
unit-tests:
|
||||
name: Unit Tests (PHP ${{ matrix.php }} - CakePHP)
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
php: ['8.1', '8.2', '8.3']
|
||||
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup PHP & Extensions
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: ${{ matrix.php }}
|
||||
extensions: mbstring, intl, dom, fileinfo, sqlite3, pdo_sqlite, pcov
|
||||
coverage: pcov
|
||||
tools: composer:v2
|
||||
|
||||
- name: Cache Composer Dependencies
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: vendor
|
||||
key: ${{ runner.os }}-php-${{ matrix.php }}-cakephp-${{ hashFiles('**/composer.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-php-${{ matrix.php }}-cakephp-
|
||||
|
||||
- name: Install Dependencies
|
||||
run: composer update --prefer-stable --prefer-dist --no-interaction --no-progress
|
||||
|
||||
- name: Run CakePHP Tests with Coverage
|
||||
run: |
|
||||
if [ -f "bin/cake" ]; then
|
||||
bin/cake test app --coverage-clover coverage.xml
|
||||
else
|
||||
./vendor/bin/phpunit --coverage-clover coverage.xml
|
||||
fi
|
||||
env:
|
||||
TMP: /tmp/
|
||||
@@ -0,0 +1,22 @@
|
||||
# GitLab CI/CD Pipeline for CakePHP Unit Testing
|
||||
image: php:8.2-cli-alpine
|
||||
|
||||
stages:
|
||||
- test
|
||||
|
||||
cache:
|
||||
key: ${CI_COMMIT_REF_SLUG}
|
||||
paths:
|
||||
- vendor/
|
||||
|
||||
unit-test:
|
||||
stage: test
|
||||
before_script:
|
||||
- apk add --no-cache sqlite-libs icu-dev libzip-dev zip unzip git $PHPIZE_DEPS
|
||||
- pecl install pcov && docker-php-ext-enable pcov
|
||||
- docker-php-ext-configure intl && docker-php-ext-install intl pdo pdo_sqlite zip
|
||||
- curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
|
||||
- composer install --prefer-dist --no-progress --no-interaction
|
||||
script:
|
||||
- if [ -f "bin/cake" ]; then bin/cake test app --coverage-text; else ./vendor/bin/phpunit --coverage-text; fi
|
||||
coverage: '/^\s*Lines:\s*\d+.\d+\%/'
|
||||
@@ -1,7 +1,7 @@
|
||||
services:
|
||||
test-runner:
|
||||
image: php:8.3-cli-alpine
|
||||
container_name: laravel_unit_tester
|
||||
container_name: php_unit_tester
|
||||
working_dir: /var/www/html
|
||||
volumes:
|
||||
- ./:/var/www/html
|
||||
@@ -15,7 +15,14 @@ services:
|
||||
- MAIL_MAILER=array
|
||||
command: >
|
||||
sh -c "
|
||||
apk add --no-cache sqlite-libs libzip-dev zip unzip git &&
|
||||
docker-php-ext-install pdo pdo_sqlite zip &&
|
||||
php artisan test
|
||||
apk add --no-cache sqlite-libs libzip-dev icu-dev zip unzip git &&
|
||||
docker-php-ext-configure intl &&
|
||||
docker-php-ext-install pdo pdo_sqlite zip intl &&
|
||||
if [ -f 'bin/cake' ]; then
|
||||
bin/cake test app;
|
||||
elif [ -f 'artisan' ]; then
|
||||
php artisan test;
|
||||
else
|
||||
vendor/bin/phpunit;
|
||||
fi
|
||||
"
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# ==============================================================================
|
||||
# Smart Test Runner for Laravel
|
||||
# Tự động nhận diện môi trường: Laravel Sail -> Docker Compose -> Local PHP
|
||||
# Smart Test Runner for Laravel & CakePHP
|
||||
# Tự động nhận diện Framework (Laravel / CakePHP) và Môi trường thực thi:
|
||||
# Laravel Sail -> Docker Compose -> Local PHP CLI / bin/cake / phpunit
|
||||
# ==============================================================================
|
||||
|
||||
set -e
|
||||
@@ -10,10 +11,12 @@ set -e
|
||||
# Màu sắc thông báo
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
RED='\033[0;31m'
|
||||
CYAN='\033[0;36m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
echo -e "${YELLOW}🔍 Đang phát hiện môi trường chạy test...${NC}"
|
||||
echo -e "${YELLOW}🔍 Đang phát hiện Framework và môi trường chạy test...${NC}"
|
||||
|
||||
# Hàm chạy test
|
||||
run_test() {
|
||||
@@ -23,42 +26,104 @@ run_test() {
|
||||
$runner "$@"
|
||||
}
|
||||
|
||||
# 1. Kiểm tra Laravel Sail
|
||||
if [ -f "./vendor/bin/sail" ] && docker info >/dev/null 2>&1; then
|
||||
if docker ps --format '{{.Names}}' | grep -q "laravel.test"; then
|
||||
echo -e "${GREEN}✓ Phát hiện Laravel Sail đang hoạt động.${NC}"
|
||||
run_test "./vendor/bin/sail test" "$@"
|
||||
# 1. Xác định Framework: CakePHP hay Laravel
|
||||
FRAMEWORK="generic"
|
||||
if [ -f "bin/cake" ] || ( [ -f "composer.json" ] && grep -q '"cakephp/cakephp"' composer.json ); then
|
||||
FRAMEWORK="cakephp"
|
||||
echo -e "${CYAN}✓ Phát hiện Framework: CakePHP${NC}"
|
||||
elif [ -f "artisan" ] || ( [ -f "composer.json" ] && grep -q '"laravel/framework"' composer.json ); then
|
||||
FRAMEWORK="laravel"
|
||||
echo -e "${CYAN}✓ Phát hiện Framework: Laravel${NC}"
|
||||
else
|
||||
echo -e "${BLUE}ℹ Framework: PHP Generic (PHPUnit)${NC}"
|
||||
fi
|
||||
|
||||
# 2. Xử lý môi trường CakePHP
|
||||
if [ "$FRAMEWORK" = "cakephp" ]; then
|
||||
# A. Docker Compose
|
||||
if [ -f "docker-compose.yml" ] || [ -f "compose.yaml" ]; then
|
||||
if docker info >/dev/null 2>&1; then
|
||||
SERVICES=("app" "web" "php" "php-fpm" "backend" "workspace")
|
||||
for svc in "${SERVICES[@]}"; do
|
||||
if docker compose ps --services --filter "status=running" 2>/dev/null | grep -q "^${svc}$"; then
|
||||
echo -e "${GREEN}✓ Phát hiện Docker Compose với service: '${svc}'${NC}"
|
||||
if docker compose exec -T ${svc} test -f "bin/cake" 2>/dev/null; then
|
||||
run_test "docker compose exec -T ${svc} bin/cake test app" "$@"
|
||||
exit 0
|
||||
else
|
||||
run_test "docker compose exec -T ${svc} vendor/bin/phpunit" "$@"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
done
|
||||
fi
|
||||
fi
|
||||
|
||||
# B. Local CakePHP CLI
|
||||
if [ -f "bin/cake" ] && command -v php >/dev/null 2>&1; then
|
||||
if ! php -m | grep -q -i intl; then
|
||||
echo -e "${YELLOW}⚠️ Cảnh báo: PHP CLI hiện tại chưa bật extension 'intl'. CakePHP yêu cầu extension này để chạy!${NC}"
|
||||
fi
|
||||
if ! php -m | grep -q -i sqlite; then
|
||||
echo -e "${YELLOW}⚠️ Cảnh báo: PHP CLI hiện tại chưa bật extension 'pdo_sqlite'. Test dùng In-memory SQLite có thể bị lỗi!${NC}"
|
||||
fi
|
||||
echo -e "${GREEN}✓ Sử dụng CakePHP CLI cục bộ (bin/cake)${NC}"
|
||||
run_test "bin/cake test app" "$@"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# C. Local PHPUnit fallback
|
||||
if [ -f "vendor/bin/phpunit" ] && command -v php >/dev/null 2>&1; then
|
||||
echo -e "${GREEN}✓ Sử dụng vendor/bin/phpunit cục bộ${NC}"
|
||||
run_test "./vendor/bin/phpunit" "$@"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# 2. Kiểm tra Docker Compose tự cấu hình
|
||||
if [ -f "docker-compose.yml" ] || [ -f "compose.yaml" ]; then
|
||||
if docker info >/dev/null 2>&1; then
|
||||
# Tìm container service chạy php
|
||||
SERVICES=("app" "php" "php-fpm" "backend" "workspace" "web")
|
||||
for svc in "${SERVICES[@]}"; do
|
||||
if docker compose ps --services --filter "status=running" 2>/dev/null | grep -q "^${svc}$"; then
|
||||
echo -e "${GREEN}✓ Phát hiện Docker Compose với service: '${svc}'${NC}"
|
||||
run_test "docker compose exec -T ${svc} php artisan test" "$@"
|
||||
exit 0
|
||||
fi
|
||||
done
|
||||
# 3. Xử lý môi trường Laravel
|
||||
if [ "$FRAMEWORK" = "laravel" ]; then
|
||||
# A. Laravel Sail
|
||||
if [ -f "./vendor/bin/sail" ] && docker info >/dev/null 2>&1; then
|
||||
if docker ps --format '{{.Names}}' | grep -q "laravel.test"; then
|
||||
echo -e "${GREEN}✓ Phát hiện Laravel Sail đang hoạt động.${NC}"
|
||||
run_test "./vendor/bin/sail test" "$@"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# B. Docker Compose
|
||||
if [ -f "docker-compose.yml" ] || [ -f "compose.yaml" ]; then
|
||||
if docker info >/dev/null 2>&1; then
|
||||
SERVICES=("app" "php" "php-fpm" "backend" "workspace" "web")
|
||||
for svc in "${SERVICES[@]}"; do
|
||||
if docker compose ps --services --filter "status=running" 2>/dev/null | grep -q "^${svc}$"; then
|
||||
echo -e "${GREEN}✓ Phát hiện Docker Compose với service: '${svc}'${NC}"
|
||||
run_test "docker compose exec -T ${svc} php artisan test" "$@"
|
||||
exit 0
|
||||
fi
|
||||
done
|
||||
fi
|
||||
fi
|
||||
|
||||
# C. Fallback về PHP CLI cục bộ
|
||||
if command -v php >/dev/null 2>&1; then
|
||||
echo -e "${YELLOW}ℹ Không tìm thấy container Docker đang chạy. Chuyển về PHP CLI máy local.${NC}"
|
||||
|
||||
if ! php -m | grep -q -i sqlite; then
|
||||
echo -e "${RED}⚠️ Cảnh báo: PHP CLI hiện tại chưa bật extension pdo_sqlite. Unit test dùng In-memory SQLite có thể bị lỗi!${NC}"
|
||||
fi
|
||||
|
||||
run_test "php artisan test" "$@"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# 3. Fallback về PHP CLI cục bộ
|
||||
if command -v php >/dev/null 2>&1; then
|
||||
echo -e "${YELLOW}ℹ Không tìm thấy container Docker đang chạy. Chuyển về PHP CLI máy local.${NC}"
|
||||
|
||||
# Kiểm tra extension pdo_sqlite
|
||||
if ! php -m | grep -q -i sqlite; then
|
||||
echo -e "${RED}⚠️ Cảnh báo: PHP CLI hiện tại chưa bật extension pdo_sqlite. Unit test dùng In-memory SQLite có thể bị lỗi!${NC}"
|
||||
fi
|
||||
|
||||
run_test "php artisan test" "$@"
|
||||
# 4. Fallback chung cho mọi dự án PHP
|
||||
if [ -f "vendor/bin/phpunit" ]; then
|
||||
echo -e "${YELLOW}ℹ Chạy qua vendor/bin/phpunit cục bộ...${NC}"
|
||||
run_test "./vendor/bin/phpunit" "$@"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo -e "${RED}❌ Không tìm thấy môi trường phù hợp (Sail, Docker Compose hoặc PHP CLI) để chạy test!${NC}"
|
||||
echo -e "${RED}❌ Không tìm thấy môi trường phù hợp (Docker Compose, Sail, bin/cake, hoặc PHP CLI) để chạy test!${NC}"
|
||||
exit 1
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# ==============================================================================
|
||||
# Smart Dependency Installer for Laravel Testing
|
||||
# Tự động phát hiện gói còn thiếu và cài đặt qua Docker (Sail / Compose) hoặc Local Composer
|
||||
# Smart Dependency Installer for Laravel & CakePHP Testing
|
||||
# Tự động phát hiện gói còn thiếu và cài đặt qua Docker hoặc Local Composer
|
||||
# ==============================================================================
|
||||
|
||||
set -e
|
||||
@@ -11,10 +11,11 @@ GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
RED='\033[0;31m'
|
||||
CYAN='\033[0;36m'
|
||||
NC='\033[0m'
|
||||
|
||||
echo -e "${BLUE}===================================================================${NC}"
|
||||
echo -e "${BLUE} Tự Động Cài Đặt Thư Viện Cần Thiết Cho Unit Test Laravel${NC}"
|
||||
echo -e "${CYAN} Tự Động Cài Đặt Thư Viện Cần Thiết Cho Unit Test (Laravel & CakePHP)${NC}"
|
||||
echo -e "${BLUE}===================================================================${NC}"
|
||||
|
||||
if [ ! -f "composer.json" ]; then
|
||||
@@ -22,14 +23,24 @@ if [ ! -f "composer.json" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 1. Xác định môi trường thực thi lệnh composer
|
||||
# 1. Xác định Framework
|
||||
FRAMEWORK="generic"
|
||||
if [ -f "bin/cake" ] || grep -q '"cakephp/cakephp"' composer.json; then
|
||||
FRAMEWORK="cakephp"
|
||||
echo -e "${GREEN}✓ Dự án: CakePHP${NC}"
|
||||
elif [ -f "artisan" ] || grep -q '"laravel/framework"' composer.json; then
|
||||
FRAMEWORK="laravel"
|
||||
echo -e "${GREEN}✓ Dự án: Laravel${NC}"
|
||||
fi
|
||||
|
||||
# 2. Xác định môi trường thực thi lệnh composer
|
||||
COMPOSER_CMD=""
|
||||
|
||||
if [ -f "./vendor/bin/sail" ] && docker info >/dev/null 2>&1 && docker ps --format '{{.Names}}' | grep -q "laravel.test"; then
|
||||
echo -e "${GREEN}✓ Môi trường: Laravel Sail đang hoạt động.${NC}"
|
||||
COMPOSER_CMD="./vendor/bin/sail composer"
|
||||
elif [ -f "docker-compose.yml" ] || [ -f "compose.yaml" ]; then
|
||||
SERVICES=("app" "php" "php-fpm" "backend" "workspace" "web")
|
||||
SERVICES=("app" "web" "php" "php-fpm" "backend" "workspace")
|
||||
for svc in "${SERVICES[@]}"; do
|
||||
if docker compose ps --services --filter "status=running" 2>/dev/null | grep -q "^${svc}$"; then
|
||||
echo -e "${GREEN}✓ Môi trường: Docker Compose (service: ${svc}).${NC}"
|
||||
@@ -43,17 +54,27 @@ if [ -z "$COMPOSER_CMD" ]; then
|
||||
if command -v composer >/dev/null 2>&1; then
|
||||
echo -e "${YELLOW}ℹ Môi trường: Composer trên máy cục bộ (Local).${NC}"
|
||||
COMPOSER_CMD="composer"
|
||||
elif [ -f "composer.phar" ]; then
|
||||
echo -e "${YELLOW}ℹ Môi trường: Sử dụng composer.phar cục bộ.${NC}"
|
||||
COMPOSER_CMD="php composer.phar"
|
||||
else
|
||||
echo -e "${RED}❌ Lỗi: Không tìm thấy Composer trên máy hoặc trong container Docker!${NC}"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# 2. Phân tích composer.json xem thiếu gói nào
|
||||
# 3. Phân tích composer.json xem thiếu gói nào
|
||||
echo -e "\n${YELLOW}🔍 Đang kiểm tra dependencies hiện có...${NC}"
|
||||
|
||||
PACKAGES_TO_INSTALL=()
|
||||
|
||||
# Kiểm tra PHPUnit nếu là CakePHP
|
||||
if [ "$FRAMEWORK" = "cakephp" ]; then
|
||||
if ! grep -q '"phpunit/phpunit"' composer.json; then
|
||||
PACKAGES_TO_INSTALL+=("phpunit/phpunit")
|
||||
fi
|
||||
fi
|
||||
|
||||
# Kiểm tra Mockery
|
||||
if ! grep -q '"mockery/mockery"' composer.json; then
|
||||
PACKAGES_TO_INSTALL+=("mockery/mockery")
|
||||
@@ -64,29 +85,31 @@ if ! grep -q '"fakerphp/faker"' composer.json; then
|
||||
PACKAGES_TO_INSTALL+=("fakerphp/faker")
|
||||
fi
|
||||
|
||||
# Kiểm tra Test Runner (Pest vs PHPUnit)
|
||||
IS_PEST=false
|
||||
if grep -q '"pestphp/pest"' composer.json; then
|
||||
IS_PEST=true
|
||||
fi
|
||||
if [ "$FRAMEWORK" = "laravel" ]; then
|
||||
# Kiểm tra Test Runner (Pest vs PHPUnit)
|
||||
IS_PEST=false
|
||||
if grep -q '"pestphp/pest"' composer.json; then
|
||||
IS_PEST=true
|
||||
fi
|
||||
|
||||
if [ "$IS_PEST" = true ]; then
|
||||
if ! grep -q '"pestphp/pest-plugin-laravel"' composer.json; then
|
||||
PACKAGES_TO_INSTALL+=("pestphp/pest-plugin-laravel")
|
||||
if [ "$IS_PEST" = true ]; then
|
||||
if ! grep -q '"pestphp/pest-plugin-laravel"' composer.json; then
|
||||
PACKAGES_TO_INSTALL+=("pestphp/pest-plugin-laravel")
|
||||
fi
|
||||
fi
|
||||
|
||||
# Kiểm tra công cụ chạy song song (Paratest)
|
||||
if ! grep -q '"brianium/paratest"' composer.json; then
|
||||
PACKAGES_TO_INSTALL+=("brianium/paratest")
|
||||
fi
|
||||
|
||||
# Kiểm tra Collision
|
||||
if ! grep -q '"nunomaduro/collision"' composer.json; then
|
||||
PACKAGES_TO_INSTALL+=("nunomaduro/collision")
|
||||
fi
|
||||
fi
|
||||
|
||||
# Kiểm tra công cụ chạy song song (Paratest)
|
||||
if ! grep -q '"brianium/paratest"' composer.json; then
|
||||
PACKAGES_TO_INSTALL+=("brianium/paratest")
|
||||
fi
|
||||
|
||||
# Kiểm tra Collision
|
||||
if ! grep -q '"nunomaduro/collision"' composer.json; then
|
||||
PACKAGES_TO_INSTALL+=("nunomaduro/collision")
|
||||
fi
|
||||
|
||||
# 3. Tiến hành cài đặt
|
||||
# 4. Tiến hành cài đặt
|
||||
if [ ${#PACKAGES_TO_INSTALL[@]} -eq 0 ]; then
|
||||
echo -e "${GREEN}🎉 Tuyệt vời! Dự án đã có đầy đủ các thư viện kiểm thử cần thiết!${NC}"
|
||||
exit 0
|
||||
|
||||
Reference in New Issue
Block a user