mirror of
https://github.com/elyby/accounts.git
synced 2026-10-20 12:48:07 +05:30
Implemented multiple captchas support. Implemented Yandex Smart Captcha support
This commit is contained in:
31 files changed
+696
-299
No files matched your search
@@ -13,8 +13,13 @@ JWT_PRIVATE_KEY_PASS=
|
||||
|
||||
## External services
|
||||
CHRLY_HOST=skinsystem.ely.by
|
||||
# Google reCAPTCHA (Google Cloud edition). Checkbox site key, API key and Google Cloud project id
|
||||
RECAPTCHA_PUBLIC=
|
||||
RECAPTCHA_SECRET=
|
||||
RECAPTCHA_PROJECT_ID=
|
||||
# Yandex SmartCaptcha. Leave empty to disable
|
||||
YANDEX_CAPTCHA_CLIENT_KEY=
|
||||
YANDEX_CAPTCHA_SERVER_KEY=
|
||||
SENTRY_DSN=
|
||||
|
||||
## SMTP params
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api\components\Captcha;
|
||||
|
||||
use api\components\Captcha\Providers\ProviderInterface;
|
||||
use InvalidArgumentException;
|
||||
|
||||
final readonly class CaptchaRegistry {
|
||||
|
||||
/**
|
||||
* @param array<string, ProviderInterface> $providers provider name => provider
|
||||
*/
|
||||
public function __construct(
|
||||
private array $providers,
|
||||
) {
|
||||
}
|
||||
|
||||
public function hasProvider(string $name): bool {
|
||||
return isset($this->providers[$name]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidArgumentException if the provider isn't registered
|
||||
*/
|
||||
public function getProvider(string $name): ProviderInterface {
|
||||
return $this->providers[$name] ?? throw new InvalidArgumentException(sprintf('The "%s" captcha provider isn\'t registered', $name));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, array<string, mixed>> provider name => its public params
|
||||
*/
|
||||
public function getPublicParams(): array {
|
||||
return array_map(fn(ProviderInterface $provider): array => $provider->getPublicParams(), $this->providers);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api\components\Captcha;
|
||||
|
||||
trait CaptchaTrait {
|
||||
|
||||
public mixed $captcha = null;
|
||||
|
||||
public mixed $captchaType = null;
|
||||
|
||||
/**
|
||||
* @return list<array<int|string, mixed>>
|
||||
*/
|
||||
protected static function getCaptchaValidationRules(): array {
|
||||
return [
|
||||
['captcha', CaptchaValidator::class],
|
||||
['captchaType', 'safe'],
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api\components\Captcha;
|
||||
|
||||
use api\components\Captcha\Providers\ProviderInterface;
|
||||
use api\components\Captcha\Providers\ReCaptchaEnterprise;
|
||||
use common\helpers\Error as E;
|
||||
use GuzzleHttp\Exception\ConnectException;
|
||||
use GuzzleHttp\Exception\ServerException;
|
||||
use Yii;
|
||||
use yii\validators\Validator;
|
||||
|
||||
final class CaptchaValidator extends Validator {
|
||||
|
||||
public const string DEFAULT_TYPE = ReCaptchaEnterprise::NAME;
|
||||
|
||||
private const int REPEAT_LIMIT = 3;
|
||||
private const int REPEAT_TIMEOUT = 1;
|
||||
|
||||
public $skipOnEmpty = false;
|
||||
|
||||
public $message = E::CAPTCHA_INVALID;
|
||||
|
||||
public string $requiredMessage = E::CAPTCHA_REQUIRED;
|
||||
|
||||
public string $typeMessage = E::CAPTCHA_INVALID;
|
||||
|
||||
/**
|
||||
* The name of the model's attribute that contains the captcha provider name
|
||||
*/
|
||||
public string $typeAttribute = 'captchaType';
|
||||
|
||||
public string $defaultType = self::DEFAULT_TYPE;
|
||||
|
||||
public function __construct(
|
||||
private readonly CaptchaRegistry $captchaRegistry,
|
||||
$config = [],
|
||||
) {
|
||||
parent::__construct($config);
|
||||
}
|
||||
|
||||
public function validateAttribute($model, $attribute): void {
|
||||
$type = $model->{$this->typeAttribute};
|
||||
$result = $this->validateCaptcha($model->$attribute, $type);
|
||||
if ($result !== null) {
|
||||
$this->addError($model, $attribute, $result[0], $result[1]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{string, array<string, string>}|null
|
||||
*/
|
||||
private function validateCaptcha(mixed $value, mixed $type): ?array {
|
||||
if (empty($value) || !is_string($value)) {
|
||||
return [$this->requiredMessage, []];
|
||||
}
|
||||
|
||||
if (empty($type)) {
|
||||
$type = $this->defaultType;
|
||||
}
|
||||
|
||||
if (!is_string($type) || !$this->captchaRegistry->hasProvider($type)) {
|
||||
return [$this->typeMessage, []];
|
||||
}
|
||||
|
||||
$provider = $this->captchaRegistry->getProvider($type);
|
||||
if (!$this->verifyWithRetries($provider, $value, Yii::$app->getRequest()->getUserIP())) {
|
||||
return [(string)$this->message, []];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function verifyWithRetries(ProviderInterface $provider, string $token, ?string $remoteIp): bool {
|
||||
$repeats = 0;
|
||||
while (true) {
|
||||
try {
|
||||
return $provider->verify($token, $remoteIp);
|
||||
} catch (ConnectException|ServerException $e) {
|
||||
if (++$repeats >= self::REPEAT_LIMIT) {
|
||||
throw $e;
|
||||
}
|
||||
|
||||
sleep(self::REPEAT_TIMEOUT);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api\components\Captcha\Providers;
|
||||
|
||||
interface ProviderInterface {
|
||||
|
||||
/**
|
||||
* Parameters the client side needs to render the captcha widget.
|
||||
* Must contain at least the "publicKey".
|
||||
*
|
||||
* @return array{publicKey: string}&array<string, mixed>
|
||||
*/
|
||||
public function getPublicParams(): array;
|
||||
|
||||
/**
|
||||
* @throws \GuzzleHttp\Exception\GuzzleException network errors must be thrown as is, so that the caller can retry the request
|
||||
* @throws \yii\base\Exception when the provider returns a malformed response
|
||||
*/
|
||||
public function verify(string $token, ?string $remoteIp): bool;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api\components\Captcha\Providers;
|
||||
|
||||
use GuzzleHttp\ClientInterface as GuzzleClientInterface;
|
||||
use GuzzleHttp\RequestOptions;
|
||||
use yii\base\Exception;
|
||||
|
||||
/**
|
||||
* Google reCAPTCHA (Google Cloud edition) with a checkbox key.
|
||||
*
|
||||
* @see https://docs.cloud.google.com/recaptcha/docs/create-assessment-website
|
||||
*/
|
||||
final readonly class ReCaptchaEnterprise implements ProviderInterface {
|
||||
|
||||
public const string NAME = 'recaptcha';
|
||||
|
||||
private const string BASE_URL = 'https://recaptchaenterprise.googleapis.com/v1';
|
||||
|
||||
public function __construct(
|
||||
private string $apiKey,
|
||||
private string $siteKey,
|
||||
private string $projectId,
|
||||
private GuzzleClientInterface $client,
|
||||
) {
|
||||
}
|
||||
|
||||
public function getPublicParams(): array {
|
||||
return [
|
||||
'publicKey' => $this->siteKey,
|
||||
];
|
||||
}
|
||||
|
||||
public function verify(string $token, ?string $remoteIp): bool {
|
||||
$event = [
|
||||
'token' => $token,
|
||||
'siteKey' => $this->siteKey,
|
||||
];
|
||||
if ($remoteIp !== null) {
|
||||
$event['userIpAddress'] = $remoteIp;
|
||||
}
|
||||
|
||||
$response = $this->client->request('POST', sprintf('%s/projects/%s/assessments', self::BASE_URL, rawurlencode($this->projectId)), [
|
||||
RequestOptions::QUERY => [
|
||||
'key' => $this->apiKey,
|
||||
],
|
||||
RequestOptions::JSON => [
|
||||
'event' => $event,
|
||||
],
|
||||
]);
|
||||
|
||||
$data = json_decode((string)$response->getBody(), true, flags: JSON_THROW_ON_ERROR);
|
||||
if (!is_array($data) || !isset($data['tokenProperties']) || !is_array($data['tokenProperties'])) {
|
||||
throw new Exception('Invalid reCAPTCHA Enterprise assessment response.');
|
||||
}
|
||||
|
||||
return ($data['tokenProperties']['valid'] ?? false) === true;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api\components\Captcha\Providers;
|
||||
|
||||
use GuzzleHttp\ClientInterface as GuzzleClientInterface;
|
||||
use GuzzleHttp\RequestOptions;
|
||||
use yii\base\Exception;
|
||||
|
||||
/**
|
||||
* @see https://yandex.cloud/en/docs/smartcaptcha/concepts/validation
|
||||
*/
|
||||
final readonly class YandexSmartCaptcha implements ProviderInterface {
|
||||
|
||||
public const string NAME = 'yandex';
|
||||
|
||||
private const string VALIDATE_URL = 'https://smartcaptcha.cloud.yandex.ru/validate';
|
||||
|
||||
public function __construct(
|
||||
private string $serverKey,
|
||||
private string $clientKey,
|
||||
private GuzzleClientInterface $client,
|
||||
) {
|
||||
}
|
||||
|
||||
public function getPublicParams(): array {
|
||||
return [
|
||||
'publicKey' => $this->clientKey,
|
||||
];
|
||||
}
|
||||
|
||||
public function verify(string $token, ?string $remoteIp): bool {
|
||||
$params = [
|
||||
'secret' => $this->serverKey,
|
||||
'token' => $token,
|
||||
];
|
||||
if ($remoteIp !== null) {
|
||||
$params['ip'] = $remoteIp;
|
||||
}
|
||||
|
||||
$response = $this->client->request('POST', self::VALIDATE_URL, [
|
||||
RequestOptions::FORM_PARAMS => $params,
|
||||
]);
|
||||
|
||||
$data = json_decode((string)$response->getBody(), true, flags: JSON_THROW_ON_ERROR);
|
||||
if (!is_array($data) || !isset($data['status'])) {
|
||||
throw new Exception('Invalid Yandex SmartCaptcha validation response.');
|
||||
}
|
||||
|
||||
return $data['status'] === 'ok';
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
<?php
|
||||
namespace api\components\ReCaptcha;
|
||||
|
||||
use yii\base\InvalidConfigException;
|
||||
|
||||
class Component extends \yii\base\Component {
|
||||
|
||||
public $public;
|
||||
|
||||
public $secret;
|
||||
|
||||
public function init(): void {
|
||||
if ($this->public === null) {
|
||||
throw new InvalidConfigException('Public is required');
|
||||
}
|
||||
|
||||
if ($this->secret === null) {
|
||||
throw new InvalidConfigException('Secret is required');
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
<?php
|
||||
namespace api\components\ReCaptcha;
|
||||
|
||||
use common\helpers\Error as E;
|
||||
use GuzzleHttp\ClientInterface;
|
||||
use GuzzleHttp\Exception\ConnectException;
|
||||
use GuzzleHttp\Exception\ServerException;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Yii;
|
||||
use yii\base\Exception;
|
||||
use yii\di\Instance;
|
||||
|
||||
class Validator extends \yii\validators\Validator {
|
||||
|
||||
private const string SITE_VERIFY_URL = 'https://recaptcha.net/recaptcha/api/siteverify';
|
||||
|
||||
private const int REPEAT_LIMIT = 3;
|
||||
private const int REPEAT_TIMEOUT = 1;
|
||||
|
||||
public $skipOnEmpty = false;
|
||||
|
||||
public $message = E::CAPTCHA_INVALID;
|
||||
|
||||
public string $requiredMessage = E::CAPTCHA_REQUIRED;
|
||||
|
||||
public Component|string $component = 'reCaptcha';
|
||||
|
||||
public function __construct(
|
||||
private readonly ClientInterface $client,
|
||||
array $config = [],
|
||||
) {
|
||||
parent::__construct($config);
|
||||
}
|
||||
|
||||
public function init(): void {
|
||||
parent::init();
|
||||
$this->component = Instance::ensure($this->component, Component::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
protected function validateValue($value): ?array {
|
||||
if (empty($value)) {
|
||||
return [$this->requiredMessage, []];
|
||||
}
|
||||
|
||||
$repeats = 0;
|
||||
do {
|
||||
$isSuccess = true;
|
||||
try {
|
||||
$response = $this->performRequest($value);
|
||||
} catch (ConnectException|ServerException $e) {
|
||||
if (++$repeats >= self::REPEAT_LIMIT) {
|
||||
throw $e;
|
||||
}
|
||||
|
||||
$isSuccess = false;
|
||||
sleep(self::REPEAT_TIMEOUT);
|
||||
}
|
||||
} while (!$isSuccess);
|
||||
|
||||
/** @noinspection PhpUndefinedVariableInspection */
|
||||
$data = json_decode($response->getBody(), true);
|
||||
if (!isset($data['success'])) {
|
||||
throw new Exception('Invalid recaptcha verify response.');
|
||||
}
|
||||
|
||||
if (!$data['success']) {
|
||||
return [$this->message, []];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \GuzzleHttp\Exception\GuzzleException
|
||||
*/
|
||||
protected function performRequest(string $value): ResponseInterface {
|
||||
return $this->client->request('POST', self::SITE_VERIFY_URL, [
|
||||
'form_params' => [
|
||||
'secret' => $this->component->secret,
|
||||
'response' => $value,
|
||||
'remoteip' => Yii::$app->getRequest()->getUserIP(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,9 +1,5 @@
|
||||
<?php
|
||||
return [
|
||||
'components' => [
|
||||
'reCaptcha' => [
|
||||
'public' => '6LfwqCYTAAAAAJlaJpqCdzESCjAXUC81Ca6jBHR7',
|
||||
'secret' => '6LfwqCYTAAAAAFPjHmgwmnjSMFeAOJh7Lk5xxDMC',
|
||||
],
|
||||
],
|
||||
];
|
||||
@@ -6,21 +6,16 @@ return [
|
||||
'privateKeyPass' => null,
|
||||
'encryptionKey' => 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
|
||||
],
|
||||
'reCaptcha' => [
|
||||
'public' => 'public-key',
|
||||
'secret' => 'private-key',
|
||||
],
|
||||
],
|
||||
'params' => [
|
||||
'authserverHost' => 'localhost',
|
||||
],
|
||||
'container' => [
|
||||
'singletons' => [
|
||||
api\components\ReCaptcha\Validator::class => fn(): api\components\ReCaptcha\Validator => new class(new GuzzleHttp\Client()) extends api\components\ReCaptcha\Validator {
|
||||
protected function validateValue($value): ?array {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
api\components\Captcha\CaptchaRegistry::class => fn(): api\components\Captcha\CaptchaRegistry => new api\components\Captcha\CaptchaRegistry([
|
||||
api\components\Captcha\Providers\ReCaptchaEnterprise::NAME => new api\tests\_support\Captcha\FakeCaptchaProvider('public-key'),
|
||||
api\components\Captcha\Providers\YandexSmartCaptcha::NAME => new api\tests\_support\Captcha\FakeCaptchaProvider('yandex-public-key'),
|
||||
]),
|
||||
common\components\SkinsSystemApi::class => fn(): common\components\SkinsSystemApi => new class('http://chrly.ely.by') extends common\components\SkinsSystemApi {
|
||||
public function textures(string $username): ?array {
|
||||
return [
|
||||
|
||||
+30
-5
@@ -22,6 +22,36 @@ return [
|
||||
'accounts' => api\modules\accounts\Module::class,
|
||||
'oauth' => api\modules\oauth\Module::class,
|
||||
],
|
||||
'container' => [
|
||||
'singletons' => [
|
||||
api\components\Captcha\CaptchaRegistry::class => function(): api\components\Captcha\CaptchaRegistry {
|
||||
$providers = [];
|
||||
if (getenv('RECAPTCHA_SECRET') && getenv('RECAPTCHA_PUBLIC') && getenv('RECAPTCHA_PROJECT_ID')) {
|
||||
$providers[api\components\Captcha\Providers\ReCaptchaEnterprise::NAME] = Yii::createObject([
|
||||
'__class' => api\components\Captcha\Providers\ReCaptchaEnterprise::class,
|
||||
'__construct()' => [
|
||||
'apiKey' => getenv('RECAPTCHA_SECRET'),
|
||||
'siteKey' => getenv('RECAPTCHA_PUBLIC'),
|
||||
'projectId' => getenv('RECAPTCHA_PROJECT_ID'),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
if (getenv('YANDEX_CAPTCHA_SERVER_KEY') && getenv('YANDEX_CAPTCHA_CLIENT_KEY')) {
|
||||
$providers[api\components\Captcha\Providers\YandexSmartCaptcha::NAME] = Yii::createObject([
|
||||
'__class' => api\components\Captcha\Providers\YandexSmartCaptcha::class,
|
||||
'__construct()' => [
|
||||
'serverKey' => getenv('YANDEX_CAPTCHA_SERVER_KEY'),
|
||||
'clientKey' => getenv('YANDEX_CAPTCHA_CLIENT_KEY'),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
// Not Yii::createObject(): this closure is the container definition of CaptchaRegistry itself
|
||||
return new api\components\Captcha\CaptchaRegistry($providers);
|
||||
},
|
||||
],
|
||||
],
|
||||
'components' => [
|
||||
'user' => [
|
||||
'class' => api\components\User\Component::class,
|
||||
@@ -90,11 +120,6 @@ return [
|
||||
'showScriptName' => false,
|
||||
'rules' => require __DIR__ . '/routes.php',
|
||||
],
|
||||
'reCaptcha' => [
|
||||
'class' => api\components\ReCaptcha\Component::class,
|
||||
'public' => getenv('RECAPTCHA_PUBLIC'),
|
||||
'secret' => getenv('RECAPTCHA_SECRET'),
|
||||
],
|
||||
'response' => [
|
||||
'format' => yii\web\Response::FORMAT_JSON,
|
||||
],
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<?php
|
||||
namespace api\controllers;
|
||||
|
||||
use api\components\Captcha\CaptchaRegistry;
|
||||
use api\components\Captcha\CaptchaValidator;
|
||||
use api\filters\NginxCache;
|
||||
use Yii;
|
||||
use yii\helpers\ArrayHelper;
|
||||
|
||||
class OptionsController extends Controller {
|
||||
@@ -27,9 +28,13 @@ class OptionsController extends Controller {
|
||||
];
|
||||
}
|
||||
|
||||
public function actionIndex(): array {
|
||||
public function actionIndex(
|
||||
CaptchaRegistry $captchaRegistry,
|
||||
): array {
|
||||
return [
|
||||
'reCaptchaPublicKey' => Yii::$app->reCaptcha->public,
|
||||
// Kept for backward compatibility, use captcha instead
|
||||
'reCaptchaPublicKey' => $captchaRegistry->getProvider(CaptchaValidator::DEFAULT_TYPE)->getPublicParams()['publicKey'],
|
||||
'captcha' => $captchaRegistry->getPublicParams(),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace api\models\authentication;
|
||||
|
||||
use api\components\ReCaptcha\Validator as ReCaptchaValidator;
|
||||
use api\components\Captcha\CaptchaTrait;
|
||||
use api\models\base\ApiForm;
|
||||
use common\components\UserFriendlyRandomKey;
|
||||
use common\helpers\Error as E;
|
||||
@@ -13,21 +13,20 @@ use common\models\EmailActivation;
|
||||
use common\tasks\SendPasswordRecoveryEmail;
|
||||
use Yii;
|
||||
use yii\base\ErrorException;
|
||||
use yii\helpers\ArrayHelper;
|
||||
|
||||
class ForgotPasswordForm extends ApiForm {
|
||||
|
||||
public mixed $captcha = null;
|
||||
use CaptchaTrait;
|
||||
|
||||
public mixed $login = null;
|
||||
|
||||
public function rules(): array {
|
||||
return [
|
||||
['captcha', ReCaptchaValidator::class],
|
||||
return ArrayHelper::merge(self::getCaptchaValidationRules(), [
|
||||
['login', 'required', 'message' => E::LOGIN_REQUIRED],
|
||||
['login', 'validateLogin'],
|
||||
['login', 'validateActivity'],
|
||||
['login', 'validateFrequency'],
|
||||
];
|
||||
]);
|
||||
}
|
||||
|
||||
public function validateLogin(string $attribute): void {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?php
|
||||
namespace api\models\authentication;
|
||||
|
||||
use api\components\ReCaptcha\Validator as ReCaptchaValidator;
|
||||
use api\components\Captcha\CaptchaTrait;
|
||||
use api\models\base\ApiForm;
|
||||
use common\components\UserFriendlyRandomKey;
|
||||
use common\helpers\Error as E;
|
||||
@@ -21,8 +21,7 @@ use yii\helpers\ArrayHelper;
|
||||
use const common\LATEST_RULES_VERSION;
|
||||
|
||||
class RegistrationForm extends ApiForm {
|
||||
|
||||
public $captcha;
|
||||
use CaptchaTrait;
|
||||
|
||||
public $username;
|
||||
|
||||
@@ -37,8 +36,7 @@ class RegistrationForm extends ApiForm {
|
||||
public $lang;
|
||||
|
||||
public function rules() {
|
||||
return [
|
||||
['captcha', ReCaptchaValidator::class],
|
||||
return ArrayHelper::merge(self::getCaptchaValidationRules(), [
|
||||
['rulesAgreement', 'required', 'message' => E::RULES_AGREEMENT_REQUIRED],
|
||||
|
||||
['username', UsernameValidator::class],
|
||||
@@ -51,7 +49,7 @@ class RegistrationForm extends ApiForm {
|
||||
|
||||
['lang', LanguageValidator::class],
|
||||
['lang', 'default', 'value' => 'en'],
|
||||
];
|
||||
]);
|
||||
}
|
||||
|
||||
public function validatePasswordAndRePasswordMatch($attribute): void {
|
||||
|
||||
@@ -3,7 +3,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace api\models\authentication;
|
||||
|
||||
use api\components\ReCaptcha\Validator as ReCaptchaValidator;
|
||||
use api\components\Captcha\CaptchaTrait;
|
||||
use api\models\base\ApiForm;
|
||||
use common\components\UserFriendlyRandomKey;
|
||||
use common\helpers\Error as E;
|
||||
@@ -13,21 +13,20 @@ use common\models\EmailActivation;
|
||||
use common\tasks\SendRegistrationEmail;
|
||||
use Webmozart\Assert\Assert;
|
||||
use Yii;
|
||||
use yii\helpers\ArrayHelper;
|
||||
|
||||
class RepeatAccountActivationForm extends ApiForm {
|
||||
|
||||
public mixed $captcha = null;
|
||||
use CaptchaTrait;
|
||||
|
||||
public mixed $email = null;
|
||||
|
||||
public function rules(): array {
|
||||
return [
|
||||
['captcha', ReCaptchaValidator::class],
|
||||
return ArrayHelper::merge(self::getCaptchaValidationRules(), [
|
||||
['email', 'filter', 'filter' => 'trim'],
|
||||
['email', 'required', 'message' => E::EMAIL_REQUIRED],
|
||||
['email', 'validateEmailForAccount'],
|
||||
['email', 'validateExistsActivation'],
|
||||
];
|
||||
]);
|
||||
}
|
||||
|
||||
public function validateEmailForAccount(string $attribute): void {
|
||||
|
||||
@@ -3,6 +3,9 @@ declare(strict_types=1);
|
||||
|
||||
namespace api\tests\_pages;
|
||||
|
||||
use api\components\Captcha\Providers\ReCaptchaEnterprise;
|
||||
use api\tests\_support\Captcha\FakeCaptchaProvider;
|
||||
|
||||
final class AuthenticationRoute extends BasePage {
|
||||
|
||||
public function login(string $login = '', string $password = '', bool|string|null $rememberMeOrToken = null, bool $rememberMe = false): void {
|
||||
@@ -24,6 +27,8 @@ final class AuthenticationRoute extends BasePage {
|
||||
$this->getActor()->sendPOST('/api/authentication/forgot-password', [
|
||||
'login' => $login,
|
||||
'totp' => $token,
|
||||
'captcha' => FakeCaptchaProvider::VALID,
|
||||
'captchaType' => ReCaptchaEnterprise::NAME,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
<?php
|
||||
namespace api\tests\_pages;
|
||||
|
||||
use api\components\Captcha\Providers\ReCaptchaEnterprise;
|
||||
use api\tests\_support\Captcha\FakeCaptchaProvider;
|
||||
|
||||
class SignupRoute extends BasePage {
|
||||
|
||||
public function register(array $registrationData): void {
|
||||
$this->getActor()->sendPOST('/api/signup', $registrationData);
|
||||
$this->getActor()->sendPOST('/api/signup', $registrationData + ['captcha' => FakeCaptchaProvider::VALID, 'captchaType' => ReCaptchaEnterprise::NAME]);
|
||||
}
|
||||
|
||||
public function sendRepeatMessage($email = ''): void {
|
||||
$this->getActor()->sendPOST('/api/signup/repeat-message', ['email' => $email]);
|
||||
$this->getActor()->sendPOST('/api/signup/repeat-message', ['email' => $email, 'captcha' => FakeCaptchaProvider::VALID, 'captchaType' => ReCaptchaEnterprise::NAME]);
|
||||
}
|
||||
|
||||
public function confirm($key = ''): void {
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api\tests\_support\Captcha;
|
||||
|
||||
use api\components\Captcha\Providers\ProviderInterface;
|
||||
|
||||
final readonly class FakeCaptchaProvider implements ProviderInterface {
|
||||
|
||||
public const string VALID = 'validCaptchaToken';
|
||||
|
||||
public function __construct(
|
||||
private string $publicKey,
|
||||
) {
|
||||
}
|
||||
|
||||
public function getPublicParams(): array {
|
||||
return [
|
||||
'publicKey' => $this->publicKey,
|
||||
];
|
||||
}
|
||||
|
||||
public function verify(string $token, ?string $remoteIp): bool {
|
||||
return $token === self::VALID;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -12,14 +12,22 @@ class OptionsCest {
|
||||
$this->route = new OptionsRoute($I);
|
||||
}
|
||||
|
||||
public function testRecaptchaPublicKey(FunctionalTester $I): void {
|
||||
$I->wantTo('Get recaptcha public key');
|
||||
public function testCaptchaPublicParams(FunctionalTester $I): void {
|
||||
$I->wantTo('Get captcha public params');
|
||||
|
||||
$this->route->get();
|
||||
$I->canSeeResponseCodeIs(200);
|
||||
$I->canSeeResponseIsJson();
|
||||
$I->canSeeResponseContainsJson([
|
||||
'reCaptchaPublicKey' => 'public-key',
|
||||
'captcha' => [
|
||||
'recaptcha' => [
|
||||
'publicKey' => 'public-key',
|
||||
],
|
||||
'yandex' => [
|
||||
'publicKey' => 'yandex-public-key',
|
||||
],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api\tests\unit\components\Captcha;
|
||||
|
||||
use api\components\Captcha\CaptchaRegistry;
|
||||
use api\components\Captcha\Providers\ProviderInterface;
|
||||
use api\tests\unit\TestCase;
|
||||
use InvalidArgumentException;
|
||||
|
||||
final class CaptchaRegistryTest extends TestCase {
|
||||
|
||||
public function testProviders(): void {
|
||||
$recaptcha = $this->createProvider('recaptcha-public');
|
||||
$yandex = $this->createProvider('yandex-public');
|
||||
$registry = new CaptchaRegistry([
|
||||
'recaptcha' => $recaptcha,
|
||||
'yandex' => $yandex,
|
||||
]);
|
||||
|
||||
$this->assertTrue($registry->hasProvider('recaptcha'));
|
||||
$this->assertTrue($registry->hasProvider('yandex'));
|
||||
$this->assertFalse($registry->hasProvider('unknown'));
|
||||
$this->assertSame($recaptcha, $registry->getProvider('recaptcha'));
|
||||
$this->assertSame($yandex, $registry->getProvider('yandex'));
|
||||
$this->assertSame([
|
||||
'recaptcha' => ['publicKey' => 'recaptcha-public'],
|
||||
'yandex' => ['publicKey' => 'yandex-public'],
|
||||
], $registry->getPublicParams());
|
||||
}
|
||||
|
||||
public function testEmptyRegistry(): void {
|
||||
$registry = new CaptchaRegistry([]);
|
||||
$this->assertFalse($registry->hasProvider('recaptcha'));
|
||||
$this->assertSame([], $registry->getPublicParams());
|
||||
}
|
||||
|
||||
public function testGetUnregisteredProvider(): void {
|
||||
$registry = new CaptchaRegistry([]);
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
$registry->getProvider('recaptcha');
|
||||
}
|
||||
|
||||
private function createProvider(string $publicKey): ProviderInterface {
|
||||
$provider = $this->createMock(ProviderInterface::class);
|
||||
$provider->method('getPublicParams')->willReturn(['publicKey' => $publicKey]);
|
||||
|
||||
return $provider;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api\tests\unit\components\Captcha;
|
||||
|
||||
use api\components\Captcha\CaptchaRegistry;
|
||||
use api\components\Captcha\CaptchaValidator;
|
||||
use api\components\Captcha\Providers\ProviderInterface;
|
||||
use api\tests\_support\Captcha\FakeCaptchaProvider;
|
||||
use api\tests\unit\TestCase;
|
||||
use GuzzleHttp\Exception\ConnectException;
|
||||
use GuzzleHttp\Exception\ServerException;
|
||||
use yii\base\Model;
|
||||
|
||||
final class CaptchaValidatorTest extends TestCase {
|
||||
|
||||
/**
|
||||
* @dataProvider getValidationCases
|
||||
*/
|
||||
public function testValidateAttribute(mixed $captcha, mixed $captchaType, ?string $expectedError): void {
|
||||
$validator = $this->createValidator([
|
||||
'recaptcha' => new FakeCaptchaProvider('recaptcha-public'),
|
||||
'yandex' => new FakeCaptchaProvider('yandex-public'),
|
||||
]);
|
||||
$model = $this->createModel($captcha, $captchaType);
|
||||
$validator->validateAttribute($model, 'captcha');
|
||||
|
||||
$this->assertSame($expectedError === null ? [] : [$expectedError], $model->getErrors('captcha'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return iterable<string, array{mixed, mixed, string|null}>
|
||||
*/
|
||||
public static function getValidationCases(): iterable {
|
||||
yield 'empty token' => ['', null, 'error.captcha_required'];
|
||||
yield 'null token' => [null, 'yandex', 'error.captcha_required'];
|
||||
yield 'non-string token' => [[FakeCaptchaProvider::VALID], null, 'error.captcha_required'];
|
||||
yield 'default provider when type is null' => [FakeCaptchaProvider::VALID, null, null];
|
||||
yield 'default provider when type is empty' => [FakeCaptchaProvider::VALID, '', null];
|
||||
yield 'explicit recaptcha' => [FakeCaptchaProvider::VALID, 'recaptcha', null];
|
||||
yield 'explicit yandex' => [FakeCaptchaProvider::VALID, 'yandex', null];
|
||||
yield 'invalid recaptcha token' => ['invalid', 'recaptcha', 'error.captcha_invalid'];
|
||||
yield 'invalid yandex token' => ['invalid', 'yandex', 'error.captcha_invalid'];
|
||||
yield 'unknown type' => [FakeCaptchaProvider::VALID, 'unknown', 'error.captcha_invalid'];
|
||||
yield 'non-string type' => [FakeCaptchaProvider::VALID, ['recaptcha'], 'error.captcha_invalid'];
|
||||
}
|
||||
|
||||
public function testValidateUsesProviderSelectedByType(): void {
|
||||
$validator = $this->createValidator([
|
||||
'yandex' => new FakeCaptchaProvider('yandex-public'),
|
||||
]);
|
||||
|
||||
$model = $this->createModel(FakeCaptchaProvider::VALID, 'yandex');
|
||||
$validator->validateAttribute($model, 'captcha');
|
||||
$this->assertFalse($model->hasErrors());
|
||||
|
||||
// The default (recaptcha) provider isn't registered, so the yandex one must not be used implicitly
|
||||
$model = $this->createModel(FakeCaptchaProvider::VALID, null);
|
||||
$validator->validateAttribute($model, 'captcha');
|
||||
$this->assertSame(['error.captcha_invalid'], $model->getErrors('captcha'));
|
||||
}
|
||||
|
||||
public function testValidateWithNetworkTroubles(): void {
|
||||
$provider = $this->createMock(ProviderInterface::class);
|
||||
$provider->expects($this->exactly(2))->method('verify')->willReturnOnConsecutiveCalls(
|
||||
$this->throwException($this->createMock(ConnectException::class)),
|
||||
true,
|
||||
);
|
||||
$this->getFunctionMock(CaptchaValidator::class, 'sleep')->expects($this->once());
|
||||
|
||||
$model = $this->createModel('token', null);
|
||||
$this->createValidator(['recaptcha' => $provider])->validateAttribute($model, 'captcha');
|
||||
$this->assertFalse($model->hasErrors());
|
||||
}
|
||||
|
||||
public function testValidateWithHugeNetworkTroubles(): void {
|
||||
$provider = $this->createMock(ProviderInterface::class);
|
||||
$provider->expects($this->exactly(3))->method('verify')->willThrowException($this->createMock(ServerException::class));
|
||||
$this->getFunctionMock(CaptchaValidator::class, 'sleep')->expects($this->exactly(2));
|
||||
|
||||
$this->expectException(ServerException::class);
|
||||
$this->createValidator(['recaptcha' => $provider])->validateAttribute($this->createModel('token', null), 'captcha');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, ProviderInterface> $providers
|
||||
*/
|
||||
private function createValidator(array $providers): CaptchaValidator {
|
||||
return new CaptchaValidator(new CaptchaRegistry($providers));
|
||||
}
|
||||
|
||||
private function createModel(mixed $captcha, mixed $captchaType): Model {
|
||||
return new class($captcha, $captchaType) extends Model {
|
||||
public function __construct(
|
||||
public mixed $captcha,
|
||||
public mixed $captchaType,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api\tests\unit\components\Captcha\Providers;
|
||||
|
||||
use api\components\Captcha\Providers\ReCaptchaEnterprise;
|
||||
use api\tests\unit\TestCase;
|
||||
use GuzzleHttp\ClientInterface as GuzzleClientInterface;
|
||||
use GuzzleHttp\Psr7\Response;
|
||||
use yii\base\Exception;
|
||||
|
||||
final class ReCaptchaEnterpriseTest extends TestCase {
|
||||
|
||||
public function testGetPublicParams(): void {
|
||||
$provider = $this->createProvider($this->createMock(GuzzleClientInterface::class));
|
||||
$this->assertSame(['publicKey' => 'site-key'], $provider->getPublicParams());
|
||||
}
|
||||
|
||||
public function testVerifyValidToken(): void {
|
||||
$client = $this->createMock(GuzzleClientInterface::class);
|
||||
$client->expects($this->once())->method('request')->with(
|
||||
'POST',
|
||||
'https://recaptchaenterprise.googleapis.com/v1/projects/my-project/assessments',
|
||||
[
|
||||
'query' => ['key' => 'api-key'],
|
||||
'json' => [
|
||||
'event' => [
|
||||
'token' => 'token',
|
||||
'siteKey' => 'site-key',
|
||||
'userIpAddress' => '127.0.0.1',
|
||||
],
|
||||
],
|
||||
],
|
||||
)->willReturn(new Response(200, [], (string)json_encode([
|
||||
'name' => 'projects/123/assessments/456',
|
||||
'tokenProperties' => [
|
||||
'valid' => true,
|
||||
'invalidReason' => 'INVALID_REASON_UNSPECIFIED',
|
||||
'hostname' => 'account.ely.by',
|
||||
'action' => '',
|
||||
],
|
||||
])));
|
||||
|
||||
$this->assertTrue($this->createProvider($client)->verify('token', '127.0.0.1'));
|
||||
}
|
||||
|
||||
public function testVerifyInvalidToken(): void {
|
||||
$client = $this->createMock(GuzzleClientInterface::class);
|
||||
$client->method('request')->willReturn(new Response(200, [], (string)json_encode([
|
||||
'tokenProperties' => [
|
||||
'valid' => false,
|
||||
'invalidReason' => 'MALFORMED',
|
||||
],
|
||||
])));
|
||||
|
||||
$this->assertFalse($this->createProvider($client)->verify('token', null));
|
||||
}
|
||||
|
||||
public function testVerifyMalformedResponse(): void {
|
||||
$client = $this->createMock(GuzzleClientInterface::class);
|
||||
$client->method('request')->willReturn(new Response(200, [], '{}'));
|
||||
|
||||
$this->expectException(Exception::class);
|
||||
$this->createProvider($client)->verify('token', null);
|
||||
}
|
||||
|
||||
private function createProvider(GuzzleClientInterface $client): ReCaptchaEnterprise {
|
||||
return new ReCaptchaEnterprise('api-key', 'site-key', 'my-project', $client);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api\tests\unit\components\Captcha\Providers;
|
||||
|
||||
use api\components\Captcha\Providers\YandexSmartCaptcha;
|
||||
use api\tests\unit\TestCase;
|
||||
use GuzzleHttp\ClientInterface;
|
||||
use GuzzleHttp\Psr7\Response;
|
||||
use yii\base\Exception;
|
||||
|
||||
final class YandexSmartCaptchaTest extends TestCase {
|
||||
|
||||
public function testGetPublicParams(): void {
|
||||
$provider = $this->createProvider($this->createMock(ClientInterface::class));
|
||||
$this->assertSame(['publicKey' => 'client-key'], $provider->getPublicParams());
|
||||
}
|
||||
|
||||
public function testVerifyValidToken(): void {
|
||||
$client = $this->createMock(ClientInterface::class);
|
||||
$client->expects($this->once())->method('request')->with(
|
||||
'POST',
|
||||
'https://smartcaptcha.cloud.yandex.ru/validate',
|
||||
[
|
||||
'form_params' => [
|
||||
'secret' => 'server-key',
|
||||
'token' => 'token',
|
||||
'ip' => '127.0.0.1',
|
||||
],
|
||||
],
|
||||
)->willReturn(new Response(200, [], (string)json_encode([
|
||||
'status' => 'ok',
|
||||
'message' => '',
|
||||
'host' => 'account.ely.by',
|
||||
])));
|
||||
|
||||
$this->assertTrue($this->createProvider($client)->verify('token', '127.0.0.1'));
|
||||
}
|
||||
|
||||
public function testVerifyInvalidToken(): void {
|
||||
$client = $this->createMock(ClientInterface::class);
|
||||
$client->method('request')->willReturn(new Response(200, [], (string)json_encode([
|
||||
'status' => 'failed',
|
||||
'message' => 'Invalid or expired Token.',
|
||||
])));
|
||||
|
||||
$this->assertFalse($this->createProvider($client)->verify('token', null));
|
||||
}
|
||||
|
||||
public function testVerifyMalformedResponse(): void {
|
||||
$client = $this->createMock(ClientInterface::class);
|
||||
$client->method('request')->willReturn(new Response(200, [], '{"message":""}'));
|
||||
|
||||
$this->expectException(Exception::class);
|
||||
$this->createProvider($client)->verify('token', null);
|
||||
}
|
||||
|
||||
private function createProvider(ClientInterface $client): YandexSmartCaptcha {
|
||||
return new YandexSmartCaptcha('server-key', 'client-key', $client);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api\tests\unit\components\ReCaptcha;
|
||||
|
||||
use api\components\ReCaptcha\Validator;
|
||||
use api\tests\unit\TestCase;
|
||||
use GuzzleHttp\ClientInterface;
|
||||
use GuzzleHttp\Exception\ConnectException;
|
||||
use GuzzleHttp\Psr7\Response;
|
||||
|
||||
class ValidatorTest extends TestCase {
|
||||
|
||||
public function testValidateEmptyValue(): void {
|
||||
$validator = new Validator($this->createMock(ClientInterface::class));
|
||||
$this->assertFalse($validator->validate('', $error));
|
||||
$this->assertSame('error.captcha_required', $error, 'Get error.captcha_required, if passed empty value');
|
||||
}
|
||||
|
||||
public function testValidateInvalidValue(): void {
|
||||
$mockClient = $this->createMock(ClientInterface::class);
|
||||
$mockClient->method('request')->willReturn(new Response(200, [], json_encode([
|
||||
'success' => false,
|
||||
'error-codes' => [
|
||||
'invalid-input-response', // The response parameter is invalid or malformed.
|
||||
],
|
||||
])));
|
||||
|
||||
$validator = new Validator($mockClient);
|
||||
$this->assertFalse($validator->validate('12341234', $error));
|
||||
$this->assertSame('error.captcha_invalid', $error, 'Get error.captcha_invalid, if passed wrong value');
|
||||
}
|
||||
|
||||
public function testValidateWithNetworkTroubles(): void {
|
||||
$mockClient = $this->createMock(ClientInterface::class);
|
||||
$mockClient->expects($this->exactly(2))->method('request')->willReturnOnConsecutiveCalls(
|
||||
$this->throwException($this->createMock(ConnectException::class)),
|
||||
$this->returnValue(new Response(200, [], json_encode([
|
||||
'success' => true,
|
||||
'error-codes' => [
|
||||
'invalid-input-response', // The response parameter is invalid or malformed.
|
||||
],
|
||||
]))),
|
||||
);
|
||||
$this->getFunctionMock(Validator::class, 'sleep')->expects($this->once());
|
||||
|
||||
$validator = new Validator($mockClient);
|
||||
$this->assertTrue($validator->validate('12341234', $error));
|
||||
$this->assertNull($error);
|
||||
}
|
||||
|
||||
public function testValidateWithHugeNetworkTroubles(): void {
|
||||
$mockClient = $this->createMock(ClientInterface::class);
|
||||
$mockClient->expects($this->exactly(3))->method('request')->willThrowException($this->createMock(ConnectException::class));
|
||||
$this->getFunctionMock(Validator::class, 'sleep')->expects($this->exactly(2));
|
||||
|
||||
$validator = new Validator($mockClient);
|
||||
$this->expectException(ConnectException::class);
|
||||
$validator->validate('12341234', $error);
|
||||
}
|
||||
|
||||
public function testValidateValidValue(): void {
|
||||
$mockClient = $this->createMock(ClientInterface::class);
|
||||
$mockClient->method('request')->willReturn(new Response(200, [], json_encode([
|
||||
'success' => true,
|
||||
])));
|
||||
$validator = new Validator($mockClient);
|
||||
$this->assertTrue($validator->validate('12341234', $error));
|
||||
$this->assertNull($error);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -3,29 +3,19 @@ declare(strict_types=1);
|
||||
|
||||
namespace api\tests\unit\models\authentication;
|
||||
|
||||
use api\components\ReCaptcha\Validator as ReCaptchaValidator;
|
||||
use api\components\Captcha\Providers\ReCaptchaEnterprise;
|
||||
use api\models\authentication\ForgotPasswordForm;
|
||||
use api\tests\_support\Captcha\FakeCaptchaProvider;
|
||||
use api\tests\unit\TestCase;
|
||||
use common\models\Account;
|
||||
use common\models\EmailActivation;
|
||||
use common\tasks\SendPasswordRecoveryEmail;
|
||||
use common\tests\fixtures\AccountFixture;
|
||||
use common\tests\fixtures\EmailActivationFixture;
|
||||
use GuzzleHttp\ClientInterface;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use Yii;
|
||||
|
||||
class ForgotPasswordFormTest extends TestCase {
|
||||
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
Yii::$container->set(ReCaptchaValidator::class, new class($this->createMock(ClientInterface::class)) extends ReCaptchaValidator {
|
||||
public function validateValue($value): ?array {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function _fixtures(): array {
|
||||
return [
|
||||
'accounts' => AccountFixture::class,
|
||||
@@ -81,7 +71,7 @@ class ForgotPasswordFormTest extends TestCase {
|
||||
public function testForgotPassword(): void {
|
||||
/** @var Account $account */
|
||||
$account = $this->tester->grabFixture('accounts', 'admin');
|
||||
$model = new ForgotPasswordForm(['login' => $account->username]);
|
||||
$model = new ForgotPasswordForm(['login' => $account->username, 'captcha' => FakeCaptchaProvider::VALID, 'captchaType' => ReCaptchaEnterprise::NAME]);
|
||||
$this->assertTrue($model->forgotPassword(), 'form should be successfully processed');
|
||||
$activation = $model->getEmailActivation();
|
||||
$this->assertInstanceOf(EmailActivation::class, $activation, 'getEmailActivation should return valid object instance');
|
||||
@@ -92,7 +82,7 @@ class ForgotPasswordFormTest extends TestCase {
|
||||
public function testForgotPasswordResend(): void {
|
||||
/** @var Account $account */
|
||||
$account = $this->tester->grabFixture('accounts', 'account-with-expired-forgot-password-message');
|
||||
$model = new ForgotPasswordForm(['login' => $account->username]);
|
||||
$model = new ForgotPasswordForm(['login' => $account->username, 'captcha' => FakeCaptchaProvider::VALID, 'captchaType' => ReCaptchaEnterprise::NAME]);
|
||||
$callTime = time();
|
||||
$this->assertTrue($model->forgotPassword(), 'form should be successfully processed');
|
||||
$emailActivation = $model->getEmailActivation();
|
||||
|
||||
@@ -3,8 +3,9 @@ declare(strict_types=1);
|
||||
|
||||
namespace api\tests\unit\models\authentication;
|
||||
|
||||
use api\components\ReCaptcha\Validator as ReCaptchaValidator;
|
||||
use api\components\Captcha\Providers\ReCaptchaEnterprise;
|
||||
use api\models\authentication\RegistrationForm;
|
||||
use api\tests\_support\Captcha\FakeCaptchaProvider;
|
||||
use api\tests\unit\TestCase;
|
||||
use common\models\Account;
|
||||
use common\models\EmailActivation;
|
||||
@@ -13,7 +14,6 @@ use common\tasks\SendRegistrationEmail;
|
||||
use common\tests\fixtures\AccountFixture;
|
||||
use common\tests\fixtures\EmailActivationFixture;
|
||||
use common\tests\fixtures\UsernameHistoryFixture;
|
||||
use GuzzleHttp\ClientInterface;
|
||||
use Yii;
|
||||
use yii\validators\EmailValidator;
|
||||
use yii\web\Request;
|
||||
@@ -24,11 +24,6 @@ class RegistrationFormTest extends TestCase {
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
$this->mockRequest();
|
||||
Yii::$container->set(ReCaptchaValidator::class, new class($this->createMock(ClientInterface::class)) extends ReCaptchaValidator {
|
||||
public function validateValue($value): ?array {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function _fixtures(): array {
|
||||
@@ -64,6 +59,8 @@ class RegistrationFormTest extends TestCase {
|
||||
'password' => 'some_password',
|
||||
'rePassword' => 'some_password',
|
||||
'rulesAgreement' => true,
|
||||
'captcha' => FakeCaptchaProvider::VALID,
|
||||
'captchaType' => ReCaptchaEnterprise::NAME,
|
||||
'lang' => 'ru',
|
||||
]);
|
||||
|
||||
@@ -82,6 +79,8 @@ class RegistrationFormTest extends TestCase {
|
||||
'password' => 'some_password',
|
||||
'rePassword' => 'some_password',
|
||||
'rulesAgreement' => true,
|
||||
'captcha' => FakeCaptchaProvider::VALID,
|
||||
'captchaType' => ReCaptchaEnterprise::NAME,
|
||||
]);
|
||||
|
||||
$account = $model->signup();
|
||||
|
||||
@@ -3,28 +3,18 @@ declare(strict_types=1);
|
||||
|
||||
namespace api\tests\unit\models\authentication;
|
||||
|
||||
use api\components\ReCaptcha\Validator as ReCaptchaValidator;
|
||||
use api\components\Captcha\Providers\ReCaptchaEnterprise;
|
||||
use api\models\authentication\RepeatAccountActivationForm;
|
||||
use api\tests\_support\Captcha\FakeCaptchaProvider;
|
||||
use api\tests\unit\TestCase;
|
||||
use common\models\Account;
|
||||
use common\models\confirmations\RegistrationConfirmation;
|
||||
use common\tasks\SendRegistrationEmail;
|
||||
use common\tests\fixtures\AccountFixture;
|
||||
use common\tests\fixtures\EmailActivationFixture;
|
||||
use GuzzleHttp\ClientInterface;
|
||||
use Yii;
|
||||
|
||||
class RepeatAccountActivationFormTest extends TestCase {
|
||||
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
Yii::$container->set(ReCaptchaValidator::class, new class($this->createMock(ClientInterface::class)) extends ReCaptchaValidator {
|
||||
public function validateValue($value): ?array {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function _fixtures(): array {
|
||||
return [
|
||||
'accounts' => AccountFixture::class,
|
||||
@@ -71,7 +61,7 @@ class RepeatAccountActivationFormTest extends TestCase {
|
||||
|
||||
/** @var \common\models\Account $account */
|
||||
$account = $this->tester->grabFixture('accounts', 'not-activated-account-with-expired-message');
|
||||
$model = new RepeatAccountActivationForm(['email' => $account->email]);
|
||||
$model = new RepeatAccountActivationForm(['email' => $account->email, 'captcha' => FakeCaptchaProvider::VALID, 'captchaType' => ReCaptchaEnterprise::NAME]);
|
||||
$this->assertTrue($model->sendRepeatMessage());
|
||||
$activation = $model->getActivation();
|
||||
$this->assertNotNull($activation);
|
||||
|
||||
@@ -33,7 +33,6 @@ abstract class BaseApplication extends yii\base\Application {
|
||||
* Include only Web application related components here
|
||||
*
|
||||
* @property \api\components\User\Component $user
|
||||
* @property \api\components\ReCaptcha\Component $reCaptcha
|
||||
* @property \api\components\Tokens\Component $tokens
|
||||
* @property \api\components\Tokens\TokensFactory $tokensFactory
|
||||
*
|
||||
|
||||
@@ -10,7 +10,7 @@ server {
|
||||
add_header X-Frame-Options "sameorigin" always;
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header Content-Security-Policy "default-src 'none';style-src 'self' 'unsafe-inline';script-src 'self' 'unsafe-inline' https://www.google-analytics.com https://recaptcha.net/recaptcha/ https://www.gstatic.com/recaptcha/ https://www.gstatic.cn/recaptcha/;img-src 'self' data: www.google-analytics.com;font-src 'self' data:;connect-src 'self' https://sentry.io https://sentry.ely.by;frame-src https://www.google.com/recaptcha/ https://recaptcha.net/recaptcha/";
|
||||
add_header Content-Security-Policy "default-src 'none';style-src 'self' 'unsafe-inline';script-src 'self' 'unsafe-inline' https://www.google-analytics.com https://recaptcha.net/recaptcha/ https://www.gstatic.com/recaptcha/ https://smartcaptcha.cloud.yandex.ru/;img-src 'self' data: www.google-analytics.com;font-src 'self' data:;connect-src 'self' https://sentry.io https://sentry.ely.by;frame-src https://recaptcha.net/recaptcha/ https://smartcaptcha.cloud.yandex.ru/";
|
||||
|
||||
# You can uncomment the next lines to enable debug mode
|
||||
# rewrite_log on;
|
||||
|
||||
+2
-37
@@ -5,31 +5,6 @@ parameters:
|
||||
count: 1
|
||||
path: api/components/ErrorHandler.php
|
||||
|
||||
-
|
||||
message: "#^Property api\\\\components\\\\ReCaptcha\\\\Component\\:\\:\\$public has no type specified\\.$#"
|
||||
count: 1
|
||||
path: api/components/ReCaptcha/Component.php
|
||||
|
||||
-
|
||||
message: "#^Property api\\\\components\\\\ReCaptcha\\\\Component\\:\\:\\$secret has no type specified\\.$#"
|
||||
count: 1
|
||||
path: api/components/ReCaptcha/Component.php
|
||||
|
||||
-
|
||||
message: "#^Cannot access property \\$secret on api\\\\components\\\\ReCaptcha\\\\Component\\|string\\.$#"
|
||||
count: 1
|
||||
path: api/components/ReCaptcha/Validator.php
|
||||
|
||||
-
|
||||
message: "#^Method api\\\\components\\\\ReCaptcha\\\\Validator\\:\\:validateValue\\(\\) should return array\\{string, array\\<string, string\\>\\|null\\}\\|null but returns array\\{string\\|null, array\\{\\}\\}\\.$#"
|
||||
count: 1
|
||||
path: api/components/ReCaptcha/Validator.php
|
||||
|
||||
-
|
||||
message: "#^Property api\\\\components\\\\ReCaptcha\\\\Validator\\:\\:\\$component \\(api\\\\components\\\\ReCaptcha\\\\Component\\|string\\) does not accept object\\.$#"
|
||||
count: 1
|
||||
path: api/components/ReCaptcha/Validator.php
|
||||
|
||||
-
|
||||
message: "#^Cannot access offset 'key' on array\\|false\\.$#"
|
||||
count: 1
|
||||
@@ -86,12 +61,12 @@ parameters:
|
||||
path: api/components/User/LegacyOAuth2Identity.php
|
||||
|
||||
-
|
||||
message: "#^Method class@anonymous/api/config/config\\-test\\.php\\:24\\:\\:profile\\(\\) return type has no value type specified in iterable type array\\.$#"
|
||||
message: "#^Method class@anonymous/api/config/config\\-test\\.php\\:19\\:\\:profile\\(\\) return type has no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: api/config/config-test.php
|
||||
|
||||
-
|
||||
message: "#^Method class@anonymous/api/config/config\\-test\\.php\\:24\\:\\:textures\\(\\) return type has no value type specified in iterable type array\\.$#"
|
||||
message: "#^Method class@anonymous/api/config/config\\-test\\.php\\:19\\:\\:textures\\(\\) return type has no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: api/config/config-test.php
|
||||
|
||||
@@ -185,11 +160,6 @@ parameters:
|
||||
count: 1
|
||||
path: api/models/authentication/RegistrationForm.php
|
||||
|
||||
-
|
||||
message: "#^Property api\\\\models\\\\authentication\\\\RegistrationForm\\:\\:\\$captcha has no type specified\\.$#"
|
||||
count: 1
|
||||
path: api/models/authentication/RegistrationForm.php
|
||||
|
||||
-
|
||||
message: "#^Property api\\\\models\\\\authentication\\\\RegistrationForm\\:\\:\\$email has no type specified\\.$#"
|
||||
count: 1
|
||||
@@ -770,11 +740,6 @@ parameters:
|
||||
count: 1
|
||||
path: api/tests/unit/TestCase.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#3 \\$body of class GuzzleHttp\\\\Psr7\\\\Response constructor expects Psr\\\\Http\\\\Message\\\\StreamInterface\\|resource\\|string\\|null, string\\|false given\\.$#"
|
||||
count: 3
|
||||
path: api/tests/unit/components/ReCaptcha/ValidatorTest.php
|
||||
|
||||
-
|
||||
message: "#^Method api\\\\tests\\\\unit\\\\components\\\\Tokens\\\\TokenReaderTest\\:\\:getAccountIdTestCases\\(\\) return type has no value type specified in iterable type iterable\\.$#"
|
||||
count: 1
|
||||
|
||||
Reference in new issue
Block a user