mirror of
https://github.com/cgsmith/yii2-user.git
synced 2026-02-04 00:02:37 -06:00
RBAC and social controller
This commit is contained in:
48
tests/FunctionalConfig.php
Normal file
48
tests/FunctionalConfig.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/_bootstrap.php';
|
||||
|
||||
return [
|
||||
'id' => 'functional-test-app',
|
||||
'basePath' => __DIR__,
|
||||
'vendorPath' => dirname(__DIR__) . '/vendor',
|
||||
'components' => [
|
||||
'db' => [
|
||||
'class' => \yii\db\Connection::class,
|
||||
'dsn' => 'sqlite:' . __DIR__ . '/_data/test.db',
|
||||
],
|
||||
'authManager' => [
|
||||
'class' => \yii\rbac\PhpManager::class,
|
||||
],
|
||||
'user' => [
|
||||
'class' => \yii\web\User::class,
|
||||
'identityClass' => \cgsmith\user\models\User::class,
|
||||
],
|
||||
'mailer' => [
|
||||
'class' => \yii\swiftmailer\Mailer::class,
|
||||
'useFileTransport' => true,
|
||||
'fileTransportPath' => '@tests/_output/mail',
|
||||
],
|
||||
'security' => [
|
||||
'class' => \yii\base\Security::class,
|
||||
],
|
||||
'request' => [
|
||||
'class' => \yii\web\Request::class,
|
||||
'cookieValidationKey' => 'test-cookie-key',
|
||||
'scriptFile' => __DIR__ . '/index.php',
|
||||
'scriptUrl' => '/index.php',
|
||||
],
|
||||
'urlManager' => [
|
||||
'class' => \yii\web\UrlManager::class,
|
||||
'enablePrettyUrl' => true,
|
||||
'showScriptName' => false,
|
||||
],
|
||||
],
|
||||
'modules' => [
|
||||
'user' => [
|
||||
'class' => \cgsmith\user\Module::class,
|
||||
],
|
||||
],
|
||||
];
|
||||
49
tests/UnitBootstrap.php
Normal file
49
tests/UnitBootstrap.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/_bootstrap.php';
|
||||
|
||||
$config = [
|
||||
'id' => 'test-app',
|
||||
'basePath' => __DIR__,
|
||||
'vendorPath' => dirname(__DIR__) . '/vendor',
|
||||
'components' => [
|
||||
'db' => [
|
||||
'class' => \yii\db\Connection::class,
|
||||
'dsn' => 'sqlite::memory:',
|
||||
],
|
||||
'authManager' => [
|
||||
'class' => \yii\rbac\PhpManager::class,
|
||||
],
|
||||
'user' => [
|
||||
'class' => \yii\web\User::class,
|
||||
'identityClass' => \cgsmith\user\models\User::class,
|
||||
],
|
||||
'mailer' => [
|
||||
'class' => \yii\swiftmailer\Mailer::class,
|
||||
'useFileTransport' => true,
|
||||
],
|
||||
'security' => [
|
||||
'class' => \yii\base\Security::class,
|
||||
],
|
||||
'request' => [
|
||||
'class' => \yii\web\Request::class,
|
||||
'cookieValidationKey' => 'test-cookie-key',
|
||||
'scriptFile' => __DIR__ . '/index.php',
|
||||
'scriptUrl' => '/index.php',
|
||||
],
|
||||
'urlManager' => [
|
||||
'class' => \yii\web\UrlManager::class,
|
||||
'enablePrettyUrl' => true,
|
||||
'showScriptName' => false,
|
||||
],
|
||||
],
|
||||
'modules' => [
|
||||
'user' => [
|
||||
'class' => \cgsmith\user\Module::class,
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
new \yii\web\Application($config);
|
||||
12
tests/_bootstrap.php
Normal file
12
tests/_bootstrap.php
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
defined('YII_DEBUG') or define('YII_DEBUG', true);
|
||||
defined('YII_ENV') or define('YII_ENV', 'test');
|
||||
|
||||
require_once __DIR__ . '/../vendor/autoload.php';
|
||||
require_once __DIR__ . '/../vendor/yiisoft/yii2/Yii.php';
|
||||
|
||||
Yii::setAlias('@cgsmith/user', dirname(__DIR__) . '/src');
|
||||
Yii::setAlias('@tests', __DIR__);
|
||||
11
tests/_support/Helper/Functional.php
Normal file
11
tests/_support/Helper/Functional.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace tests\_support\Helper;
|
||||
|
||||
use Codeception\Module;
|
||||
|
||||
class Functional extends Module
|
||||
{
|
||||
}
|
||||
11
tests/_support/Helper/Unit.php
Normal file
11
tests/_support/Helper/Unit.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace tests\_support\Helper;
|
||||
|
||||
use Codeception\Module;
|
||||
|
||||
class Unit extends Module
|
||||
{
|
||||
}
|
||||
8
tests/functional.suite.yml
Normal file
8
tests/functional.suite.yml
Normal file
@@ -0,0 +1,8 @@
|
||||
actor: FunctionalTester
|
||||
modules:
|
||||
enabled:
|
||||
- Yii2:
|
||||
part: [orm, fixtures]
|
||||
configFile: 'FunctionalConfig.php'
|
||||
- Asserts
|
||||
- \tests\_support\Helper\Functional
|
||||
6
tests/unit.suite.yml
Normal file
6
tests/unit.suite.yml
Normal file
@@ -0,0 +1,6 @@
|
||||
actor: UnitTester
|
||||
modules:
|
||||
enabled:
|
||||
- Asserts
|
||||
- \tests\_support\Helper\Unit
|
||||
bootstrap: UnitBootstrap.php
|
||||
185
tests/unit/ModuleTest.php
Normal file
185
tests/unit/ModuleTest.php
Normal file
@@ -0,0 +1,185 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace tests\unit;
|
||||
|
||||
use Codeception\Test\Unit;
|
||||
use cgsmith\user\Module;
|
||||
|
||||
class ModuleTest extends Unit
|
||||
{
|
||||
private Module $module;
|
||||
|
||||
protected function _before(): void
|
||||
{
|
||||
$this->module = new Module('user');
|
||||
}
|
||||
|
||||
public function testVersionReturnsString(): void
|
||||
{
|
||||
$this->assertIsString($this->module->getVersion());
|
||||
$this->assertEquals(Module::VERSION, $this->module->getVersion());
|
||||
}
|
||||
|
||||
public function testDefaultModelMapReturnsUserClass(): void
|
||||
{
|
||||
$userClass = $this->module->getModelClass('User');
|
||||
$this->assertEquals('cgsmith\user\models\User', $userClass);
|
||||
}
|
||||
|
||||
public function testDefaultModelMapReturnsProfileClass(): void
|
||||
{
|
||||
$profileClass = $this->module->getModelClass('Profile');
|
||||
$this->assertEquals('cgsmith\user\models\Profile', $profileClass);
|
||||
}
|
||||
|
||||
public function testDefaultModelMapReturnsTokenClass(): void
|
||||
{
|
||||
$tokenClass = $this->module->getModelClass('Token');
|
||||
$this->assertEquals('cgsmith\user\models\Token', $tokenClass);
|
||||
}
|
||||
|
||||
public function testDefaultModelMapReturnsLoginFormClass(): void
|
||||
{
|
||||
$loginFormClass = $this->module->getModelClass('LoginForm');
|
||||
$this->assertEquals('cgsmith\user\models\LoginForm', $loginFormClass);
|
||||
}
|
||||
|
||||
public function testCustomModelMapOverridesDefault(): void
|
||||
{
|
||||
$this->module->modelMap = [
|
||||
'User' => 'app\models\CustomUser',
|
||||
];
|
||||
|
||||
$userClass = $this->module->getModelClass('User');
|
||||
$this->assertEquals('app\models\CustomUser', $userClass);
|
||||
}
|
||||
|
||||
public function testGetModelClassThrowsExceptionForUnknownModel(): void
|
||||
{
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage('Unknown model: NonExistent');
|
||||
|
||||
$this->module->getModelClass('NonExistent');
|
||||
}
|
||||
|
||||
public function testEmailChangeStrategyConstants(): void
|
||||
{
|
||||
$this->assertEquals(0, Module::EMAIL_CHANGE_INSECURE);
|
||||
$this->assertEquals(1, Module::EMAIL_CHANGE_DEFAULT);
|
||||
$this->assertEquals(2, Module::EMAIL_CHANGE_SECURE);
|
||||
}
|
||||
|
||||
public function testDefaultConfiguration(): void
|
||||
{
|
||||
$this->assertTrue($this->module->enableRegistration);
|
||||
$this->assertTrue($this->module->enableConfirmation);
|
||||
$this->assertFalse($this->module->enableUnconfirmedLogin);
|
||||
$this->assertTrue($this->module->enablePasswordRecovery);
|
||||
$this->assertFalse($this->module->enableGdpr);
|
||||
$this->assertFalse($this->module->enableTwoFactor);
|
||||
$this->assertFalse($this->module->enableSocialAuth);
|
||||
$this->assertFalse($this->module->enableCaptcha);
|
||||
$this->assertFalse($this->module->enableSessionHistory);
|
||||
}
|
||||
|
||||
public function testDefaultPasswordSettings(): void
|
||||
{
|
||||
$this->assertEquals(8, $this->module->minPasswordLength);
|
||||
$this->assertEquals(72, $this->module->maxPasswordLength);
|
||||
$this->assertEquals(12, $this->module->cost);
|
||||
}
|
||||
|
||||
public function testDefaultTokenExpiration(): void
|
||||
{
|
||||
$this->assertEquals(86400, $this->module->confirmWithin);
|
||||
$this->assertEquals(21600, $this->module->recoverWithin);
|
||||
$this->assertEquals(1209600, $this->module->rememberFor);
|
||||
}
|
||||
|
||||
public function testDefaultAvatarSettings(): void
|
||||
{
|
||||
$this->assertTrue($this->module->enableGravatar);
|
||||
$this->assertTrue($this->module->enableAvatarUpload);
|
||||
$this->assertEquals('@webroot/uploads/avatars', $this->module->avatarPath);
|
||||
$this->assertEquals('@web/uploads/avatars', $this->module->avatarUrl);
|
||||
$this->assertEquals(2097152, $this->module->maxAvatarSize);
|
||||
$this->assertEquals(['jpg', 'jpeg', 'png', 'gif', 'webp'], $this->module->avatarExtensions);
|
||||
}
|
||||
|
||||
public function testDefaultUrlPrefix(): void
|
||||
{
|
||||
$this->assertEquals('user', $this->module->urlPrefix);
|
||||
}
|
||||
|
||||
public function testMailerSenderWithDefault(): void
|
||||
{
|
||||
$sender = $this->module->getMailerSender();
|
||||
$this->assertIsArray($sender);
|
||||
}
|
||||
|
||||
public function testMailerSenderWithCustomConfig(): void
|
||||
{
|
||||
$this->module->mailer = [
|
||||
'sender' => ['custom@example.com' => 'Custom Sender'],
|
||||
];
|
||||
|
||||
$sender = $this->module->getMailerSender();
|
||||
$this->assertEquals(['custom@example.com' => 'Custom Sender'], $sender);
|
||||
}
|
||||
|
||||
public function testDefaultCaptchaSettings(): void
|
||||
{
|
||||
$this->assertEquals('yii', $this->module->captchaType);
|
||||
$this->assertEquals(['register'], $this->module->captchaForms);
|
||||
$this->assertEquals(0.5, $this->module->reCaptchaV3Threshold);
|
||||
}
|
||||
|
||||
public function testDefaultTwoFactorSettings(): void
|
||||
{
|
||||
$this->assertEquals('', $this->module->twoFactorIssuer);
|
||||
$this->assertEquals(10, $this->module->twoFactorBackupCodesCount);
|
||||
$this->assertFalse($this->module->twoFactorRequireForAdmins);
|
||||
}
|
||||
|
||||
public function testDefaultGdprSettings(): void
|
||||
{
|
||||
$this->assertEquals('1.0', $this->module->gdprConsentVersion);
|
||||
$this->assertNull($this->module->gdprConsentUrl);
|
||||
$this->assertEquals([], $this->module->gdprExemptRoutes);
|
||||
$this->assertTrue($this->module->requireGdprConsentBeforeRegistration);
|
||||
}
|
||||
|
||||
public function testDefaultSessionSettings(): void
|
||||
{
|
||||
$this->assertEquals(10, $this->module->sessionHistoryLimit);
|
||||
$this->assertFalse($this->module->enableSessionSeparation);
|
||||
$this->assertEquals('BACKENDSESSID', $this->module->backendSessionName);
|
||||
$this->assertEquals('PHPSESSID', $this->module->frontendSessionName);
|
||||
}
|
||||
|
||||
public function testDefaultSocialAuthSettings(): void
|
||||
{
|
||||
$this->assertTrue($this->module->enableSocialRegistration);
|
||||
$this->assertTrue($this->module->enableSocialConnect);
|
||||
}
|
||||
|
||||
public function testDefaultRbacSettings(): void
|
||||
{
|
||||
$this->assertFalse($this->module->enableRbacManagement);
|
||||
$this->assertNull($this->module->rbacManagementPermission);
|
||||
$this->assertNull($this->module->adminPermission);
|
||||
$this->assertNull($this->module->impersonatePermission);
|
||||
}
|
||||
|
||||
public function testDefaultActiveFormClass(): void
|
||||
{
|
||||
$this->assertEquals('yii\widgets\ActiveForm', $this->module->activeFormClass);
|
||||
}
|
||||
|
||||
public function testAdminsArrayIsEmpty(): void
|
||||
{
|
||||
$this->assertEquals([], $this->module->admins);
|
||||
}
|
||||
}
|
||||
139
tests/unit/helpers/PasswordTest.php
Normal file
139
tests/unit/helpers/PasswordTest.php
Normal file
@@ -0,0 +1,139 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace tests\unit\helpers;
|
||||
|
||||
use Codeception\Test\Unit;
|
||||
use cgsmith\user\helpers\Password;
|
||||
|
||||
class PasswordTest extends Unit
|
||||
{
|
||||
public function testHashCreatesValidHash(): void
|
||||
{
|
||||
$password = 'testPassword123!';
|
||||
$hash = Password::hash($password);
|
||||
|
||||
$this->assertNotEmpty($hash);
|
||||
$this->assertNotEquals($password, $hash);
|
||||
$this->assertStringStartsWith('$2y$', $hash);
|
||||
}
|
||||
|
||||
public function testHashWithCustomCost(): void
|
||||
{
|
||||
$password = 'testPassword123!';
|
||||
$hash = Password::hash($password, 10);
|
||||
|
||||
$this->assertNotEmpty($hash);
|
||||
$this->assertTrue(Password::validate($password, $hash));
|
||||
}
|
||||
|
||||
public function testValidateReturnsTrueForCorrectPassword(): void
|
||||
{
|
||||
$password = 'testPassword123!';
|
||||
$hash = Password::hash($password);
|
||||
|
||||
$this->assertTrue(Password::validate($password, $hash));
|
||||
}
|
||||
|
||||
public function testValidateReturnsFalseForIncorrectPassword(): void
|
||||
{
|
||||
$password = 'testPassword123!';
|
||||
$wrongPassword = 'wrongPassword456!';
|
||||
$hash = Password::hash($password);
|
||||
|
||||
$this->assertFalse(Password::validate($wrongPassword, $hash));
|
||||
}
|
||||
|
||||
public function testGenerateCreatesPasswordOfSpecifiedLength(): void
|
||||
{
|
||||
$length = 16;
|
||||
$password = Password::generate($length);
|
||||
|
||||
$this->assertEquals($length, strlen($password));
|
||||
}
|
||||
|
||||
public function testGenerateDefaultLength(): void
|
||||
{
|
||||
$password = Password::generate();
|
||||
|
||||
$this->assertEquals(12, strlen($password));
|
||||
}
|
||||
|
||||
public function testGenerateCreatesUniquePasswords(): void
|
||||
{
|
||||
$passwords = [];
|
||||
for ($i = 0; $i < 10; $i++) {
|
||||
$passwords[] = Password::generate();
|
||||
}
|
||||
|
||||
$uniquePasswords = array_unique($passwords);
|
||||
$this->assertCount(10, $uniquePasswords);
|
||||
}
|
||||
|
||||
public function testCheckStrengthWeakPassword(): void
|
||||
{
|
||||
$result = Password::checkStrength('abc');
|
||||
|
||||
$this->assertEquals(0, $result['score']);
|
||||
$this->assertNotEmpty($result['feedback']);
|
||||
}
|
||||
|
||||
public function testCheckStrengthMediumPassword(): void
|
||||
{
|
||||
$result = Password::checkStrength('Password1');
|
||||
|
||||
$this->assertGreaterThan(1, $result['score']);
|
||||
$this->assertLessThanOrEqual(4, $result['score']);
|
||||
}
|
||||
|
||||
public function testCheckStrengthStrongPassword(): void
|
||||
{
|
||||
$result = Password::checkStrength('MyStr0ng!Passw0rd');
|
||||
|
||||
$this->assertEquals(4, $result['score']);
|
||||
$this->assertEmpty($result['feedback']);
|
||||
}
|
||||
|
||||
public function testCheckStrengthFeedbackForShortPassword(): void
|
||||
{
|
||||
$result = Password::checkStrength('short');
|
||||
|
||||
$this->assertContains('Password should be at least 8 characters.', $result['feedback']);
|
||||
}
|
||||
|
||||
public function testCheckStrengthFeedbackForNoUppercase(): void
|
||||
{
|
||||
$result = Password::checkStrength('lowercase123!');
|
||||
|
||||
$this->assertContains('Add uppercase letters.', $result['feedback']);
|
||||
}
|
||||
|
||||
public function testCheckStrengthFeedbackForNoLowercase(): void
|
||||
{
|
||||
$result = Password::checkStrength('UPPERCASE123!');
|
||||
|
||||
$this->assertContains('Add lowercase letters.', $result['feedback']);
|
||||
}
|
||||
|
||||
public function testCheckStrengthFeedbackForNoNumbers(): void
|
||||
{
|
||||
$result = Password::checkStrength('NoNumbersHere!');
|
||||
|
||||
$this->assertContains('Add numbers.', $result['feedback']);
|
||||
}
|
||||
|
||||
public function testCheckStrengthFeedbackForNoSpecialChars(): void
|
||||
{
|
||||
$result = Password::checkStrength('NoSpecial123');
|
||||
|
||||
$this->assertContains('Add special characters.', $result['feedback']);
|
||||
}
|
||||
|
||||
public function testCheckStrengthMaxScoreIsFour(): void
|
||||
{
|
||||
$result = Password::checkStrength('ThisIsAnExtremelyLongAndSecurePassword123!@#');
|
||||
|
||||
$this->assertEquals(4, $result['score']);
|
||||
}
|
||||
}
|
||||
108
tests/unit/services/CaptchaServiceTest.php
Normal file
108
tests/unit/services/CaptchaServiceTest.php
Normal file
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace tests\unit\services;
|
||||
|
||||
use Codeception\Test\Unit;
|
||||
use cgsmith\user\Module;
|
||||
use cgsmith\user\services\CaptchaService;
|
||||
|
||||
class CaptchaServiceTest extends Unit
|
||||
{
|
||||
private Module $module;
|
||||
private CaptchaService $service;
|
||||
|
||||
protected function _before(): void
|
||||
{
|
||||
$this->module = new Module('user');
|
||||
$this->service = new CaptchaService($this->module);
|
||||
}
|
||||
|
||||
public function testIsEnabledForFormReturnsFalseWhenCaptchaDisabled(): void
|
||||
{
|
||||
$this->module->enableCaptcha = false;
|
||||
|
||||
$this->assertFalse($this->service->isEnabledForForm('login'));
|
||||
$this->assertFalse($this->service->isEnabledForForm('register'));
|
||||
$this->assertFalse($this->service->isEnabledForForm('recovery'));
|
||||
}
|
||||
|
||||
public function testIsEnabledForFormReturnsTrueWhenFormInList(): void
|
||||
{
|
||||
$this->module->enableCaptcha = true;
|
||||
$this->module->captchaForms = ['login', 'register'];
|
||||
|
||||
$this->assertTrue($this->service->isEnabledForForm('login'));
|
||||
$this->assertTrue($this->service->isEnabledForForm('register'));
|
||||
$this->assertFalse($this->service->isEnabledForForm('recovery'));
|
||||
}
|
||||
|
||||
public function testGetCaptchaTypeReturnsConfiguredType(): void
|
||||
{
|
||||
$this->module->captchaType = CaptchaService::TYPE_RECAPTCHA_V2;
|
||||
|
||||
$this->assertEquals(CaptchaService::TYPE_RECAPTCHA_V2, $this->service->getCaptchaType());
|
||||
}
|
||||
|
||||
public function testGetSiteKeyReturnsNullForYiiType(): void
|
||||
{
|
||||
$this->module->captchaType = CaptchaService::TYPE_YII;
|
||||
|
||||
$this->assertNull($this->service->getSiteKey());
|
||||
}
|
||||
|
||||
public function testGetSiteKeyReturnsReCaptchaKey(): void
|
||||
{
|
||||
$this->module->captchaType = CaptchaService::TYPE_RECAPTCHA_V2;
|
||||
$this->module->reCaptchaSiteKey = 'test-site-key';
|
||||
|
||||
$this->assertEquals('test-site-key', $this->service->getSiteKey());
|
||||
}
|
||||
|
||||
public function testGetSiteKeyReturnsReCaptchaV3Key(): void
|
||||
{
|
||||
$this->module->captchaType = CaptchaService::TYPE_RECAPTCHA_V3;
|
||||
$this->module->reCaptchaSiteKey = 'test-v3-site-key';
|
||||
|
||||
$this->assertEquals('test-v3-site-key', $this->service->getSiteKey());
|
||||
}
|
||||
|
||||
public function testGetSiteKeyReturnsHCaptchaKey(): void
|
||||
{
|
||||
$this->module->captchaType = CaptchaService::TYPE_HCAPTCHA;
|
||||
$this->module->hCaptchaSiteKey = 'hcaptcha-site-key';
|
||||
|
||||
$this->assertEquals('hcaptcha-site-key', $this->service->getSiteKey());
|
||||
}
|
||||
|
||||
public function testGetReCaptchaActionReturnsCorrectActions(): void
|
||||
{
|
||||
$this->assertEquals('login', $this->service->getReCaptchaAction('login'));
|
||||
$this->assertEquals('register', $this->service->getReCaptchaAction('register'));
|
||||
$this->assertEquals('recovery', $this->service->getReCaptchaAction('recovery'));
|
||||
$this->assertEquals('submit', $this->service->getReCaptchaAction('unknown'));
|
||||
}
|
||||
|
||||
public function testVerifyReCaptchaReturnsFalseWithoutSecretKey(): void
|
||||
{
|
||||
$this->module->reCaptchaSecretKey = null;
|
||||
|
||||
$this->assertFalse($this->service->verifyReCaptcha('test-response'));
|
||||
}
|
||||
|
||||
public function testVerifyHCaptchaReturnsFalseWithoutSecretKey(): void
|
||||
{
|
||||
$this->module->hCaptchaSecretKey = null;
|
||||
|
||||
$this->assertFalse($this->service->verifyHCaptcha('test-response'));
|
||||
}
|
||||
|
||||
public function testCaptchaTypeConstants(): void
|
||||
{
|
||||
$this->assertEquals('yii', CaptchaService::TYPE_YII);
|
||||
$this->assertEquals('recaptcha-v2', CaptchaService::TYPE_RECAPTCHA_V2);
|
||||
$this->assertEquals('recaptcha-v3', CaptchaService::TYPE_RECAPTCHA_V3);
|
||||
$this->assertEquals('hcaptcha', CaptchaService::TYPE_HCAPTCHA);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user