mirror of
https://github.com/elyby/accounts.git
synced 2024-11-23 05:33:20 +05:30
Rework email_activation model, get rid of behaviors, use json column to store additional data
This commit is contained in:
parent
22e8158581
commit
666213afc7
@ -88,10 +88,10 @@ class AuthenticationController extends Controller {
|
||||
];
|
||||
|
||||
if (ArrayHelper::getValue($data['errors'], 'login') === E::RECENTLY_SENT_MESSAGE) {
|
||||
/** @var \common\models\confirmations\ForgotPassword $emailActivation */
|
||||
$emailActivation = $model->getEmailActivation();
|
||||
$data['data'] = [
|
||||
'canRepeatIn' => $emailActivation->canRepeatIn(),
|
||||
'repeatFrequency' => $emailActivation->repeatTimeout,
|
||||
'canRepeatIn' => $emailActivation->canResendAt()->getTimestamp(),
|
||||
];
|
||||
}
|
||||
|
||||
@ -102,8 +102,7 @@ class AuthenticationController extends Controller {
|
||||
$response = [
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'canRepeatIn' => $emailActivation->canRepeatIn(),
|
||||
'repeatFrequency' => $emailActivation->repeatTimeout,
|
||||
'canRepeatIn' => $emailActivation->canResendAt()->getTimestamp(),
|
||||
],
|
||||
];
|
||||
|
||||
|
@ -62,10 +62,10 @@ class SignupController extends Controller {
|
||||
];
|
||||
|
||||
if (ArrayHelper::getValue($response['errors'], 'email') === E::RECENTLY_SENT_MESSAGE) {
|
||||
/** @var \common\models\confirmations\RegistrationConfirmation $activation */
|
||||
$activation = $model->getActivation();
|
||||
$response['data'] = [
|
||||
'canRepeatIn' => $activation->canRepeatIn(),
|
||||
'repeatFrequency' => $activation->repeatTimeout,
|
||||
'canRepeatIn' => $activation->canResendAt(),
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -51,7 +51,7 @@ class ForgotPasswordForm extends ApiForm {
|
||||
public function validateFrequency(string $attribute): void {
|
||||
if (!$this->hasErrors()) {
|
||||
$emailConfirmation = $this->getEmailActivation();
|
||||
if ($emailConfirmation !== null && !$emailConfirmation->canRepeat()) {
|
||||
if ($emailConfirmation !== null && !$emailConfirmation->canResend()) {
|
||||
$this->addError($attribute, E::RECENTLY_SENT_MESSAGE);
|
||||
}
|
||||
}
|
||||
@ -89,7 +89,7 @@ class ForgotPasswordForm extends ApiForm {
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getEmailActivation(): ?EmailActivation {
|
||||
public function getEmailActivation(): ?ForgotPassword {
|
||||
$account = $this->getAccount();
|
||||
if ($account === null) {
|
||||
return null;
|
||||
|
@ -50,7 +50,7 @@ class RepeatAccountActivationForm extends ApiForm {
|
||||
public function validateExistsActivation(string $attribute): void {
|
||||
if (!$this->hasErrors()) {
|
||||
$activation = $this->getActivation();
|
||||
if ($activation !== null && !$activation->canRepeat()) {
|
||||
if ($activation !== null && !$activation->canResend()) {
|
||||
$this->addError($attribute, E::RECENTLY_SENT_MESSAGE);
|
||||
}
|
||||
}
|
||||
@ -94,7 +94,7 @@ class RepeatAccountActivationForm extends ApiForm {
|
||||
->one();
|
||||
}
|
||||
|
||||
public function getActivation(): ?EmailActivation {
|
||||
public function getActivation(): ?RegistrationConfirmation {
|
||||
return $this->getAccount()
|
||||
->getEmailActivations()
|
||||
->withType(EmailActivation::TYPE_REGISTRATION_EMAIL_CONFIRMATION)
|
||||
|
@ -17,11 +17,11 @@ class EmailVerificationAction extends BaseAccountAction {
|
||||
return [];
|
||||
}
|
||||
|
||||
/** @var \common\models\EmailActivation $emailActivation */
|
||||
$emailActivation = $model->getEmailActivation();
|
||||
|
||||
return [
|
||||
'canRepeatIn' => $emailActivation->canRepeatIn(),
|
||||
'repeatFrequency' => $emailActivation->repeatTimeout,
|
||||
'canRepeatIn' => $emailActivation->canResendAt()->getTimestamp(),
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -31,7 +31,7 @@ class SendEmailVerificationForm extends AccountActionForm {
|
||||
public function validateFrequency(string $attribute): void {
|
||||
if (!$this->hasErrors()) {
|
||||
$emailConfirmation = $this->getEmailActivation();
|
||||
if ($emailConfirmation !== null && !$emailConfirmation->canRepeat()) {
|
||||
if ($emailConfirmation !== null && !$emailConfirmation->canResend()) {
|
||||
$this->addError($attribute, E::RECENTLY_SENT_MESSAGE);
|
||||
}
|
||||
}
|
||||
@ -80,6 +80,8 @@ class SendEmailVerificationForm extends AccountActionForm {
|
||||
* The method is designed to check if the E-mail change messages are sent too often.
|
||||
* Including checking for the confirmation of the new E-mail type, because when you go to this step,
|
||||
* the activation of the previous step is removed.
|
||||
*
|
||||
* @return CurrentEmailConfirmation|\common\models\confirmations\NewEmailConfirmation
|
||||
*/
|
||||
public function getEmailActivation(): ?EmailActivation {
|
||||
return $this->getAccount()
|
||||
|
@ -57,7 +57,6 @@ class ForgotPasswordCest {
|
||||
],
|
||||
]);
|
||||
$I->canSeeResponseJsonMatchesJsonPath('$.data.canRepeatIn');
|
||||
$I->canSeeResponseJsonMatchesJsonPath('$.data.repeatFrequency');
|
||||
}
|
||||
|
||||
/**
|
||||
@ -68,7 +67,6 @@ class ForgotPasswordCest {
|
||||
'success' => true,
|
||||
]);
|
||||
$I->canSeeResponseJsonMatchesJsonPath('$.data.canRepeatIn');
|
||||
$I->canSeeResponseJsonMatchesJsonPath('$.data.repeatFrequency');
|
||||
if ($expectEmailMask) {
|
||||
$I->canSeeResponseJsonMatchesJsonPath('$.data.emailMask');
|
||||
}
|
||||
|
@ -53,7 +53,6 @@ class RepeatAccountActivationCest {
|
||||
],
|
||||
]);
|
||||
$I->canSeeResponseJsonMatchesJsonPath('$.data.canRepeatIn');
|
||||
$I->canSeeResponseJsonMatchesJsonPath('$.data.repeatFrequency');
|
||||
}
|
||||
|
||||
public function testSuccess(FunctionalTester $I) {
|
||||
|
@ -39,7 +39,6 @@ class ChangeEmailInitializeCest {
|
||||
],
|
||||
]);
|
||||
$I->canSeeResponseJsonMatchesJsonPath('$.data.canRepeatIn');
|
||||
$I->canSeeResponseJsonMatchesJsonPath('$.data.repeatFrequency');
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ use api\components\ReCaptcha\Validator as ReCaptchaValidator;
|
||||
use api\models\authentication\ForgotPasswordForm;
|
||||
use api\tests\unit\TestCase;
|
||||
use common\models\Account;
|
||||
use common\models\confirmations\ForgotPassword;
|
||||
use common\models\EmailActivation;
|
||||
use common\tasks\SendPasswordRecoveryEmail;
|
||||
use common\tests\fixtures\AccountFixture;
|
||||
@ -126,7 +127,7 @@ class ForgotPasswordFormTest extends TestCase {
|
||||
return new class($params) extends ForgotPasswordForm {
|
||||
public $key;
|
||||
|
||||
public function getEmailActivation(): ?EmailActivation {
|
||||
public function getEmailActivation(): ?ForgotPassword {
|
||||
return EmailActivation::findOne(['key' => $this->key]);
|
||||
}
|
||||
};
|
||||
|
@ -7,6 +7,7 @@ use api\components\ReCaptcha\Validator as ReCaptchaValidator;
|
||||
use api\models\authentication\RepeatAccountActivationForm;
|
||||
use api\tests\unit\TestCase;
|
||||
use Codeception\Specify;
|
||||
use common\models\confirmations\RegistrationConfirmation;
|
||||
use common\models\EmailActivation;
|
||||
use common\tasks\SendRegistrationEmail;
|
||||
use common\tests\fixtures\AccountFixture;
|
||||
@ -100,7 +101,7 @@ class RepeatAccountActivationFormTest extends TestCase {
|
||||
return new class($params) extends RepeatAccountActivationForm {
|
||||
public $emailKey;
|
||||
|
||||
public function getActivation(): ?EmailActivation {
|
||||
public function getActivation(): ?RegistrationConfirmation {
|
||||
return EmailActivation::findOne($this->emailKey);
|
||||
}
|
||||
};
|
||||
|
@ -1,4 +1,6 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api\tests\unit\modules\accounts\models;
|
||||
|
||||
use api\modules\accounts\models\ChangeEmailForm;
|
||||
@ -20,16 +22,17 @@ class ChangeEmailFormTest extends TestCase {
|
||||
public function testChangeEmail() {
|
||||
/** @var Account $account */
|
||||
$account = Account::findOne($this->getAccountId());
|
||||
/** @var EmailActivation $newEmailConfirmationFixture */
|
||||
$newEmailConfirmationFixture = $this->tester->grabFixture('emailActivations', 'newEmailConfirmation');
|
||||
$model = new ChangeEmailForm($account, [
|
||||
'key' => $newEmailConfirmationFixture['key'],
|
||||
'key' => $newEmailConfirmationFixture->key,
|
||||
]);
|
||||
$this->assertTrue($model->performAction());
|
||||
$this->assertNull(EmailActivation::findOne([
|
||||
'account_id' => $account->id,
|
||||
'type' => EmailActivation::TYPE_NEW_EMAIL_CONFIRMATION,
|
||||
]));
|
||||
$data = unserialize($newEmailConfirmationFixture['_data']);
|
||||
$data = $newEmailConfirmationFixture->data;
|
||||
$this->assertSame($data['newEmail'], $account->email);
|
||||
}
|
||||
|
||||
|
@ -25,7 +25,7 @@ class EmailActivationKeyValidatorTest extends TestCase {
|
||||
->getMock();
|
||||
|
||||
$expiredActivation = new ForgotPassword();
|
||||
$expiredActivation->created_at = time() - $expiredActivation->expirationTimeout - 10;
|
||||
$expiredActivation->created_at = time() - 60 * 60 - 10;
|
||||
|
||||
$validActivation = new EmailActivation();
|
||||
|
||||
|
@ -37,7 +37,7 @@ class EmailActivationKeyValidator extends Validator {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($activation->isExpired()) {
|
||||
if ($activation->isStale()) {
|
||||
$this->addError($model, $attribute, $this->expired);
|
||||
return;
|
||||
}
|
||||
|
@ -1,48 +0,0 @@
|
||||
<?php
|
||||
namespace common\behaviors;
|
||||
|
||||
use yii\base\Behavior;
|
||||
use yii\helpers\ArrayHelper;
|
||||
|
||||
class DataBehavior extends Behavior {
|
||||
|
||||
/**
|
||||
* @var string attribute name to which this behavior will be applied
|
||||
*/
|
||||
public $attribute = '_data';
|
||||
|
||||
/**
|
||||
* @param string $key
|
||||
* @param mixed $value
|
||||
*/
|
||||
protected function setKey(string $key, $value) {
|
||||
$data = $this->getData();
|
||||
$data[$key] = $value;
|
||||
$this->owner->{$this->attribute} = serialize($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $key
|
||||
* @return mixed
|
||||
*/
|
||||
protected function getKey(string $key) {
|
||||
return ArrayHelper::getValue($this->getData(), $key);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
* @throws \yii\base\ErrorException Yii2 will catch Notice from the wrong deserialization and turn it
|
||||
* into its own Exception, so that the program can continue to work normally (you still should catch an Exception)
|
||||
*/
|
||||
private function getData() {
|
||||
$data = $this->owner->{$this->attribute};
|
||||
if (is_string($data)) {
|
||||
$data = unserialize($data);
|
||||
} else {
|
||||
$data = [];
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
}
|
@ -1,75 +0,0 @@
|
||||
<?php
|
||||
namespace common\behaviors;
|
||||
|
||||
use yii\base\Behavior;
|
||||
|
||||
/**
|
||||
* @property \common\models\EmailActivation $owner
|
||||
*/
|
||||
class EmailActivationExpirationBehavior extends Behavior {
|
||||
|
||||
/**
|
||||
* @var int the number of seconds before the code can be sent again
|
||||
* @see EmailActivation::canRepeat()
|
||||
*/
|
||||
public $repeatTimeout;
|
||||
|
||||
/**
|
||||
* @var int the number of seconds before this activation expires
|
||||
* @see EmailActivation::isExpired()
|
||||
*/
|
||||
public $expirationTimeout;
|
||||
|
||||
/**
|
||||
* Is it allowed to resend a message of the current type?
|
||||
* The value of EmailActivation::$repeatTimeout is used for checking as follows:
|
||||
* - <0 will forbid you to resend this activation
|
||||
* - =0 allows you to send messages at any time
|
||||
* - >0 will check how many seconds have passed since the model was created
|
||||
*
|
||||
* @see EmailActivation::compareTime()
|
||||
* @return bool
|
||||
*/
|
||||
public function canRepeat(): bool {
|
||||
return $this->compareTime($this->repeatTimeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* Did the code expire?
|
||||
* The value of EmailActivation::$expirationTimeout is used for checking as follows:
|
||||
* - <0 means the code will never expire
|
||||
* - =0 will always say that the code has expired
|
||||
* - >0 will check how many seconds have passed since the model was created
|
||||
*
|
||||
* @see EmailActivation::compareTime()
|
||||
* @return bool
|
||||
*/
|
||||
public function isExpired(): bool {
|
||||
return $this->compareTime($this->expirationTimeout);
|
||||
}
|
||||
|
||||
public function canRepeatIn(): int {
|
||||
return $this->calculateTime($this->repeatTimeout);
|
||||
}
|
||||
|
||||
public function expireIn(): int {
|
||||
return $this->calculateTime($this->expirationTimeout);
|
||||
}
|
||||
|
||||
private function compareTime(int $value): bool {
|
||||
if ($value < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($value === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return time() > $this->calculateTime($value);
|
||||
}
|
||||
|
||||
private function calculateTime(int $value): int {
|
||||
return $this->owner->created_at + $value;
|
||||
}
|
||||
|
||||
}
|
@ -3,10 +3,10 @@ declare(strict_types=1);
|
||||
|
||||
namespace common\models;
|
||||
|
||||
use common\behaviors\DataBehavior;
|
||||
use common\behaviors\EmailActivationExpirationBehavior;
|
||||
use common\behaviors\PrimaryKeyValueBehavior;
|
||||
use common\components\UserFriendlyRandomKey;
|
||||
use DateInterval;
|
||||
use DateTimeImmutable;
|
||||
use yii\base\InvalidConfigException;
|
||||
use yii\behaviors\TimestampBehavior;
|
||||
use yii\db\ActiveRecord;
|
||||
@ -15,18 +15,16 @@ use yii\helpers\ArrayHelper;
|
||||
/**
|
||||
* Fields:
|
||||
* @property string $key
|
||||
* @property integer $account_id
|
||||
* @property integer $type
|
||||
* @property string $_data
|
||||
* @property integer $created_at
|
||||
* @property int $account_id
|
||||
* @property int $type
|
||||
* @property array|null $data
|
||||
* @property int $created_at
|
||||
*
|
||||
* Relations:
|
||||
* @property Account $account
|
||||
*
|
||||
* Behaviors:
|
||||
* @mixin TimestampBehavior
|
||||
* @mixin EmailActivationExpirationBehavior
|
||||
* @mixin DataBehavior
|
||||
*/
|
||||
class EmailActivation extends ActiveRecord {
|
||||
|
||||
@ -39,41 +37,15 @@ class EmailActivation extends ActiveRecord {
|
||||
return 'email_activations';
|
||||
}
|
||||
|
||||
public static function find(): EmailActivationQuery {
|
||||
return new EmailActivationQuery(static::class);
|
||||
}
|
||||
|
||||
public function behaviors(): array {
|
||||
public static function getClassMap(): array {
|
||||
return [
|
||||
[
|
||||
'class' => TimestampBehavior::class,
|
||||
'updatedAtAttribute' => false,
|
||||
],
|
||||
[
|
||||
'class' => PrimaryKeyValueBehavior::class,
|
||||
'value' => function() {
|
||||
return UserFriendlyRandomKey::make();
|
||||
},
|
||||
],
|
||||
'expirationBehavior' => [
|
||||
'class' => EmailActivationExpirationBehavior::class,
|
||||
'repeatTimeout' => 5 * 60, // 5m
|
||||
'expirationTimeout' => -1,
|
||||
],
|
||||
'dataBehavior' => [
|
||||
'class' => DataBehavior::class,
|
||||
'attribute' => '_data',
|
||||
],
|
||||
self::TYPE_REGISTRATION_EMAIL_CONFIRMATION => confirmations\RegistrationConfirmation::class,
|
||||
self::TYPE_FORGOT_PASSWORD_KEY => confirmations\ForgotPassword::class,
|
||||
self::TYPE_CURRENT_EMAIL_CONFIRMATION => confirmations\CurrentEmailConfirmation::class,
|
||||
self::TYPE_NEW_EMAIL_CONFIRMATION => confirmations\NewEmailConfirmation::class,
|
||||
];
|
||||
}
|
||||
|
||||
public function getAccount(): AccountQuery {
|
||||
return $this->hasOne(Account::class, ['id' => 'account_id']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public static function instantiate($row) {
|
||||
$type = ArrayHelper::getValue($row, 'type');
|
||||
if ($type === null) {
|
||||
@ -88,13 +60,79 @@ class EmailActivation extends ActiveRecord {
|
||||
return new $classMap[$type]();
|
||||
}
|
||||
|
||||
public static function getClassMap(): array {
|
||||
public static function find(): EmailActivationQuery {
|
||||
return new EmailActivationQuery(static::class);
|
||||
}
|
||||
|
||||
public function behaviors(): array {
|
||||
return [
|
||||
self::TYPE_REGISTRATION_EMAIL_CONFIRMATION => confirmations\RegistrationConfirmation::class,
|
||||
self::TYPE_FORGOT_PASSWORD_KEY => confirmations\ForgotPassword::class,
|
||||
self::TYPE_CURRENT_EMAIL_CONFIRMATION => confirmations\CurrentEmailConfirmation::class,
|
||||
self::TYPE_NEW_EMAIL_CONFIRMATION => confirmations\NewEmailConfirmation::class,
|
||||
[
|
||||
'class' => TimestampBehavior::class,
|
||||
'updatedAtAttribute' => false,
|
||||
],
|
||||
[
|
||||
'class' => PrimaryKeyValueBehavior::class,
|
||||
'value' => function(): string {
|
||||
return UserFriendlyRandomKey::make();
|
||||
},
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function getAccount(): AccountQuery {
|
||||
/** @noinspection PhpIncompatibleReturnTypeInspection */
|
||||
return $this->hasOne(Account::class, ['id' => 'account_id']);
|
||||
}
|
||||
|
||||
public function canResend(): bool {
|
||||
$timeout = $this->getResendTimeout();
|
||||
if ($timeout === null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $this->compareTime($timeout);
|
||||
}
|
||||
|
||||
public function canResendAt(): DateTimeImmutable {
|
||||
return $this->calculateTime($this->getResendTimeout() ?? new DateInterval('PT0S'));
|
||||
}
|
||||
|
||||
public function isStale(): bool {
|
||||
$duration = $this->getExpireDuration();
|
||||
if ($duration === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->compareTime($duration);
|
||||
}
|
||||
|
||||
/**
|
||||
* After which time the message for this action type can be resended.
|
||||
* When null returned the message can be sent immediately.
|
||||
*
|
||||
* @return DateInterval|null
|
||||
*/
|
||||
protected function getResendTimeout(): ?DateInterval {
|
||||
return new DateInterval('PT5M');
|
||||
}
|
||||
|
||||
/**
|
||||
* How long the activation code should be valid.
|
||||
* When null returned the code is never expires
|
||||
*
|
||||
* @return DateInterval|null
|
||||
*/
|
||||
protected function getExpireDuration(): ?DateInterval {
|
||||
return null;
|
||||
}
|
||||
|
||||
private function compareTime(DateInterval $value): bool {
|
||||
return (new DateTimeImmutable()) > $this->calculateTime($value);
|
||||
}
|
||||
|
||||
private function calculateTime(DateInterval $interval): DateTimeImmutable {
|
||||
/** @noinspection PhpUnhandledExceptionInspection */
|
||||
return (new DateTimeImmutable('@' . $this->created_at))->add($interval);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ namespace common\models\confirmations;
|
||||
|
||||
use common\models\EmailActivation;
|
||||
use common\models\EmailActivationQuery;
|
||||
use yii\helpers\ArrayHelper;
|
||||
use DateInterval;
|
||||
|
||||
class CurrentEmailConfirmation extends EmailActivation {
|
||||
|
||||
@ -13,18 +13,17 @@ class CurrentEmailConfirmation extends EmailActivation {
|
||||
return parent::find()->withType(EmailActivation::TYPE_CURRENT_EMAIL_CONFIRMATION);
|
||||
}
|
||||
|
||||
public function behaviors(): array {
|
||||
return ArrayHelper::merge(parent::behaviors(), [
|
||||
'expirationBehavior' => [
|
||||
'repeatTimeout' => 6 * 60 * 60, // 6h
|
||||
'expirationTimeout' => 1 * 60 * 60, // 1h
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function init(): void {
|
||||
parent::init();
|
||||
$this->type = EmailActivation::TYPE_CURRENT_EMAIL_CONFIRMATION;
|
||||
}
|
||||
|
||||
protected function getResendTimeout(): ?DateInterval {
|
||||
return new DateInterval('PT6H');
|
||||
}
|
||||
|
||||
protected function getExpireDuration(): ?DateInterval {
|
||||
return new DateInterval('PT1H');
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ namespace common\models\confirmations;
|
||||
|
||||
use common\models\EmailActivation;
|
||||
use common\models\EmailActivationQuery;
|
||||
use yii\helpers\ArrayHelper;
|
||||
use DateInterval;
|
||||
|
||||
class ForgotPassword extends EmailActivation {
|
||||
|
||||
@ -13,18 +13,17 @@ class ForgotPassword extends EmailActivation {
|
||||
return parent::find()->withType(EmailActivation::TYPE_FORGOT_PASSWORD_KEY);
|
||||
}
|
||||
|
||||
public function behaviors(): array {
|
||||
return ArrayHelper::merge(parent::behaviors(), [
|
||||
'expirationBehavior' => [
|
||||
'repeatTimeout' => 30 * 60,
|
||||
'expirationTimeout' => 1 * 60 * 60,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function init(): void {
|
||||
parent::init();
|
||||
$this->type = EmailActivation::TYPE_FORGOT_PASSWORD_KEY;
|
||||
}
|
||||
|
||||
protected function getResendTimeout(): ?DateInterval {
|
||||
return new DateInterval('PT30M');
|
||||
}
|
||||
|
||||
protected function getExpireDuration(): ?DateInterval {
|
||||
return new DateInterval('PT1H');
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -7,30 +7,25 @@ use common\models\EmailActivation;
|
||||
use common\models\EmailActivationQuery;
|
||||
use yii\helpers\ArrayHelper;
|
||||
|
||||
/**
|
||||
* Behaviors:
|
||||
* @mixin NewEmailConfirmationBehavior
|
||||
*/
|
||||
class NewEmailConfirmation extends EmailActivation {
|
||||
|
||||
public static function find(): EmailActivationQuery {
|
||||
return parent::find()->withType(EmailActivation::TYPE_NEW_EMAIL_CONFIRMATION);
|
||||
}
|
||||
|
||||
public function behaviors(): array {
|
||||
return ArrayHelper::merge(parent::behaviors(), [
|
||||
'expirationBehavior' => [
|
||||
'repeatTimeout' => 5 * 60,
|
||||
],
|
||||
'dataBehavior' => [
|
||||
'class' => NewEmailConfirmationBehavior::class,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function init(): void {
|
||||
parent::init();
|
||||
$this->type = EmailActivation::TYPE_NEW_EMAIL_CONFIRMATION;
|
||||
}
|
||||
|
||||
public function getNewEmail(): string {
|
||||
return $this->data['newEmail'];
|
||||
}
|
||||
|
||||
public function setNewEmail(string $newEmail): void {
|
||||
$this->data = ArrayHelper::merge($this->data ?? [], [
|
||||
'newEmail' => $newEmail,
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,21 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace common\models\confirmations;
|
||||
|
||||
use common\behaviors\DataBehavior;
|
||||
|
||||
/**
|
||||
* @property string $newEmail
|
||||
*/
|
||||
class NewEmailConfirmationBehavior extends DataBehavior {
|
||||
|
||||
public function getNewEmail(): string {
|
||||
return $this->getKey('newEmail');
|
||||
}
|
||||
|
||||
public function setNewEmail(string $newEmail): void {
|
||||
$this->setKey('newEmail', $newEmail);
|
||||
}
|
||||
|
||||
}
|
30
common/tests/fixtures/data/email-activations.php
vendored
30
common/tests/fixtures/data/email-activations.php
vendored
@ -1,46 +1,56 @@
|
||||
<?php
|
||||
|
||||
use Carbon\Carbon;
|
||||
use common\models\EmailActivation;
|
||||
|
||||
return [
|
||||
'freshRegistrationConfirmation' => [
|
||||
'key' => 'HABGCABHJ1234HBHVD',
|
||||
'account_id' => 3,
|
||||
'type' => \common\models\EmailActivation::TYPE_REGISTRATION_EMAIL_CONFIRMATION,
|
||||
'type' => EmailActivation::TYPE_REGISTRATION_EMAIL_CONFIRMATION,
|
||||
'data' => null,
|
||||
'created_at' => time(),
|
||||
],
|
||||
'oldRegistrationConfirmation' => [
|
||||
'key' => 'H23HBDCHHAG2HGHGHS',
|
||||
'account_id' => 4,
|
||||
'type' => \common\models\EmailActivation::TYPE_REGISTRATION_EMAIL_CONFIRMATION,
|
||||
'created_at' => time() - (new \common\models\confirmations\RegistrationConfirmation())->repeatTimeout - 10,
|
||||
'type' => EmailActivation::TYPE_REGISTRATION_EMAIL_CONFIRMATION,
|
||||
'data' => null,
|
||||
'created_at' => Carbon::now()->subMinutes(5)->subSeconds(10)->unix(),
|
||||
],
|
||||
'freshPasswordRecovery' => [
|
||||
'key' => 'H24HBDCHHAG2HGHGHS',
|
||||
'account_id' => 5,
|
||||
'type' => \common\models\EmailActivation::TYPE_FORGOT_PASSWORD_KEY,
|
||||
'type' => EmailActivation::TYPE_FORGOT_PASSWORD_KEY,
|
||||
'data' => null,
|
||||
'created_at' => time(),
|
||||
],
|
||||
'oldPasswordRecovery' => [
|
||||
'key' => 'H25HBDCHHAG2HGHGHS',
|
||||
'account_id' => 6,
|
||||
'type' => \common\models\EmailActivation::TYPE_FORGOT_PASSWORD_KEY,
|
||||
'created_at' => time() - (new \common\models\confirmations\ForgotPassword())->repeatTimeout - 10,
|
||||
'type' => EmailActivation::TYPE_FORGOT_PASSWORD_KEY,
|
||||
'data' => null,
|
||||
'created_at' => Carbon::now()->subMinutes(30)->subSeconds(10)->unix(),
|
||||
],
|
||||
'currentChangeEmailConfirmation' => [
|
||||
'key' => 'H27HBDCHHAG2HGHGHS',
|
||||
'account_id' => 7,
|
||||
'type' => \common\models\EmailActivation::TYPE_CURRENT_EMAIL_CONFIRMATION,
|
||||
'type' => EmailActivation::TYPE_CURRENT_EMAIL_CONFIRMATION,
|
||||
'data' => null,
|
||||
'created_at' => time() - 10,
|
||||
],
|
||||
'newEmailConfirmation' => [
|
||||
'key' => 'H28HBDCHHAG2HGHGHS',
|
||||
'account_id' => 8,
|
||||
'type' => \common\models\EmailActivation::TYPE_NEW_EMAIL_CONFIRMATION,
|
||||
'_data' => serialize(['newEmail' => 'my-new-email@ely.by']),
|
||||
'type' => EmailActivation::TYPE_NEW_EMAIL_CONFIRMATION,
|
||||
'data' => ['newEmail' => 'my-new-email@ely.by'],
|
||||
'created_at' => time() - 10,
|
||||
],
|
||||
'deeplyExpiredConfirmation' => [
|
||||
'key' => 'H29HBDCHHAG2HGHGHS',
|
||||
'account_id' => 1,
|
||||
'type' => \common\models\EmailActivation::TYPE_NEW_EMAIL_CONFIRMATION,
|
||||
'type' => EmailActivation::TYPE_NEW_EMAIL_CONFIRMATION,
|
||||
'data' => null,
|
||||
'created_at' => 1487695872,
|
||||
],
|
||||
];
|
||||
|
@ -1,75 +0,0 @@
|
||||
<?php
|
||||
namespace common\tests\unit\behaviors;
|
||||
|
||||
use Codeception\Specify;
|
||||
use common\behaviors\DataBehavior;
|
||||
use common\tests\_support\ProtectedCaller;
|
||||
use common\tests\unit\TestCase;
|
||||
use yii\base\ErrorException;
|
||||
use yii\base\Model;
|
||||
|
||||
class DataBehaviorTest extends TestCase {
|
||||
use Specify;
|
||||
use ProtectedCaller;
|
||||
|
||||
public function testSetKey() {
|
||||
$model = $this->createModel();
|
||||
/** @var DataBehavior $behavior */
|
||||
$behavior = $model->behaviors['dataBehavior'];
|
||||
$this->callProtected($behavior, 'setKey', 'my-key', 'my-value');
|
||||
$this->assertSame(serialize(['my-key' => 'my-value']), $model->_data);
|
||||
}
|
||||
|
||||
public function testGetKey() {
|
||||
$model = $this->createModel();
|
||||
$model->_data = serialize(['some-key' => 'some-value']);
|
||||
/** @var DataBehavior $behavior */
|
||||
$behavior = $model->behaviors['dataBehavior'];
|
||||
$this->assertSame('some-value', $this->callProtected($behavior, 'getKey', 'some-key'));
|
||||
}
|
||||
|
||||
public function testGetData() {
|
||||
$this->specify('getting value from null field should return empty array', function() {
|
||||
$model = $this->createModel();
|
||||
/** @var DataBehavior $behavior */
|
||||
$behavior = $model->behaviors['dataBehavior'];
|
||||
$this->assertSame([], $this->callProtected($behavior, 'getData'));
|
||||
});
|
||||
|
||||
$this->specify('getting value from serialized data field should return encoded value', function() {
|
||||
$model = $this->createModel();
|
||||
$data = ['foo' => 'bar'];
|
||||
$model->_data = serialize($data);
|
||||
/** @var DataBehavior $behavior */
|
||||
$behavior = $model->behaviors['dataBehavior'];
|
||||
$this->assertSame($data, $this->callProtected($behavior, 'getData'));
|
||||
});
|
||||
|
||||
$this->specify('getting value from invalid serialization string', function() {
|
||||
$model = $this->createModel();
|
||||
$model->_data = 'this is invalid serialization of string';
|
||||
/** @var DataBehavior $behavior */
|
||||
$behavior = $model->behaviors['dataBehavior'];
|
||||
$this->expectException(ErrorException::class);
|
||||
$this->callProtected($behavior, 'getData');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Model
|
||||
*/
|
||||
private function createModel() {
|
||||
return new class extends Model {
|
||||
public $_data;
|
||||
|
||||
public function behaviors() {
|
||||
return [
|
||||
'dataBehavior' => [
|
||||
'class' => DataBehavior::class,
|
||||
],
|
||||
];
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
@ -1,109 +0,0 @@
|
||||
<?php
|
||||
namespace common\tests\unit\behaviors;
|
||||
|
||||
use Codeception\Specify;
|
||||
use common\behaviors\EmailActivationExpirationBehavior;
|
||||
use common\tests\_support\ProtectedCaller;
|
||||
use common\tests\unit\TestCase;
|
||||
use yii\base\Model;
|
||||
|
||||
class EmailActivationExpirationBehaviorTest extends TestCase {
|
||||
use Specify;
|
||||
use ProtectedCaller;
|
||||
|
||||
public function testCalculateTime() {
|
||||
$behavior = $this->createBehavior();
|
||||
$time = time();
|
||||
$behavior->owner->created_at = $time;
|
||||
$this->assertSame($time + 10, $this->callProtected($behavior, 'calculateTime', 10));
|
||||
}
|
||||
|
||||
public function testCompareTime() {
|
||||
$this->specify('expect false, if passed value is less then 0', function() {
|
||||
$behavior = $this->createBehavior();
|
||||
$this->assertFalse($this->callProtected($behavior, 'compareTime', -1));
|
||||
});
|
||||
|
||||
$this->specify('expect true, if passed value is equals 0', function() {
|
||||
$behavior = $this->createBehavior();
|
||||
$this->assertTrue($this->callProtected($behavior, 'compareTime', 0));
|
||||
});
|
||||
|
||||
$this->specify('expect true, if passed value is more than 0 and current time is greater then calculated', function() {
|
||||
$behavior = $this->createBehavior();
|
||||
$behavior->owner->created_at = time() - 10;
|
||||
$this->assertTrue($this->callProtected($behavior, 'compareTime', 5));
|
||||
});
|
||||
|
||||
$this->specify('expect false, if passed value is more than 0 and current time is less then calculated', function() {
|
||||
$behavior = $this->createBehavior();
|
||||
$behavior->owner->created_at = time() - 2;
|
||||
$this->assertFalse($this->callProtected($behavior, 'compareTime', 7));
|
||||
});
|
||||
}
|
||||
|
||||
public function testCanRepeat() {
|
||||
$this->specify('we can repeat, if created_at + repeatTimeout is greater, then current time', function() {
|
||||
$behavior = $this->createBehavior();
|
||||
$behavior->repeatTimeout = 30;
|
||||
$behavior->owner->created_at = time() - 60;
|
||||
$this->assertTrue($behavior->canRepeat());
|
||||
});
|
||||
|
||||
$this->specify('we cannot repeat, if created_at + repeatTimeout is less, then current time', function() {
|
||||
$behavior = $this->createBehavior();
|
||||
$behavior->repeatTimeout = 60;
|
||||
$behavior->owner->created_at = time() - 30;
|
||||
$this->assertFalse($behavior->canRepeat());
|
||||
});
|
||||
}
|
||||
|
||||
public function testIsExpired() {
|
||||
$this->specify('key is not expired, if created_at + expirationTimeout is greater, then current time', function() {
|
||||
$behavior = $this->createBehavior();
|
||||
$behavior->expirationTimeout = 30;
|
||||
$behavior->owner->created_at = time() - 60;
|
||||
$this->assertTrue($behavior->isExpired());
|
||||
});
|
||||
|
||||
$this->specify('key is not expired, if created_at + expirationTimeout is less, then current time', function() {
|
||||
$behavior = $this->createBehavior();
|
||||
$behavior->expirationTimeout = 60;
|
||||
$behavior->owner->created_at = time() - 30;
|
||||
$this->assertFalse($behavior->isExpired());
|
||||
});
|
||||
}
|
||||
|
||||
public function testCanRepeatIn() {
|
||||
$this->specify('get expected timestamp for repeat time moment', function() {
|
||||
$behavior = $this->createBehavior();
|
||||
$behavior->repeatTimeout = 30;
|
||||
$behavior->owner->created_at = time() - 60;
|
||||
$this->assertSame($behavior->owner->created_at + $behavior->repeatTimeout, $behavior->canRepeatIn());
|
||||
});
|
||||
}
|
||||
|
||||
public function testExpireIn() {
|
||||
$this->specify('get expected timestamp for key expire moment', function() {
|
||||
$behavior = $this->createBehavior();
|
||||
$behavior->expirationTimeout = 30;
|
||||
$behavior->owner->created_at = time() - 60;
|
||||
$this->assertSame($behavior->owner->created_at + $behavior->expirationTimeout, $behavior->expireIn());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @return EmailActivationExpirationBehavior
|
||||
*/
|
||||
private function createBehavior() {
|
||||
$behavior = new EmailActivationExpirationBehavior();
|
||||
/** @var Model $model */
|
||||
$model = new class extends Model {
|
||||
public $created_at;
|
||||
};
|
||||
$model->attachBehavior('email-activation-behavior', $behavior);
|
||||
|
||||
return $behavior;
|
||||
}
|
||||
|
||||
}
|
@ -1,10 +1,14 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace common\tests\unit\models;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use common\models\confirmations;
|
||||
use common\models\EmailActivation;
|
||||
use common\tests\fixtures\EmailActivationFixture;
|
||||
use common\tests\unit\TestCase;
|
||||
use DateInterval;
|
||||
|
||||
class EmailActivationTest extends TestCase {
|
||||
|
||||
@ -14,22 +18,59 @@ class EmailActivationTest extends TestCase {
|
||||
];
|
||||
}
|
||||
|
||||
public function testInstantiate() {
|
||||
$this->assertInstanceOf(confirmations\RegistrationConfirmation::class, EmailActivation::findOne([
|
||||
'type' => EmailActivation::TYPE_REGISTRATION_EMAIL_CONFIRMATION,
|
||||
]));
|
||||
/**
|
||||
* @dataProvider getInstantiateTestCases
|
||||
*/
|
||||
public function testInstantiate(int $type, string $expectedClassType) {
|
||||
$this->assertInstanceOf($expectedClassType, EmailActivation::findOne(['type' => $type]));
|
||||
}
|
||||
|
||||
$this->assertInstanceOf(confirmations\ForgotPassword::class, EmailActivation::findOne([
|
||||
'type' => EmailActivation::TYPE_FORGOT_PASSWORD_KEY,
|
||||
]));
|
||||
public function getInstantiateTestCases() {
|
||||
yield [EmailActivation::TYPE_REGISTRATION_EMAIL_CONFIRMATION, confirmations\RegistrationConfirmation::class];
|
||||
yield [EmailActivation::TYPE_FORGOT_PASSWORD_KEY, confirmations\ForgotPassword::class];
|
||||
yield [EmailActivation::TYPE_CURRENT_EMAIL_CONFIRMATION, confirmations\CurrentEmailConfirmation::class];
|
||||
yield [EmailActivation::TYPE_NEW_EMAIL_CONFIRMATION, confirmations\NewEmailConfirmation::class];
|
||||
}
|
||||
|
||||
$this->assertInstanceOf(confirmations\CurrentEmailConfirmation::class, EmailActivation::findOne([
|
||||
'type' => EmailActivation::TYPE_CURRENT_EMAIL_CONFIRMATION,
|
||||
]));
|
||||
public function testCanResend() {
|
||||
$model = $this->createPartialMock(EmailActivation::class, ['getResendTimeout']);
|
||||
$model->method('getResendTimeout')->willReturn(new DateInterval('PT10M'));
|
||||
|
||||
$this->assertInstanceOf(confirmations\NewEmailConfirmation::class, EmailActivation::findOne([
|
||||
'type' => EmailActivation::TYPE_NEW_EMAIL_CONFIRMATION,
|
||||
]));
|
||||
$model->created_at = time();
|
||||
$this->assertFalse($model->canResend());
|
||||
$this->assertEqualsWithDelta(Carbon::now()->addMinutes(10), $model->canResendAt(), 3);
|
||||
|
||||
$model->created_at = time() - 60 * 10 - 1;
|
||||
$this->assertTrue($model->canResend());
|
||||
$this->assertEqualsWithDelta(Carbon::now()->subSecond(), $model->canResendAt(), 3);
|
||||
}
|
||||
|
||||
public function testCanResendWithNullTimeout() {
|
||||
$model = $this->createPartialMock(EmailActivation::class, ['getResendTimeout']);
|
||||
$model->method('getResendTimeout')->willReturn(null);
|
||||
|
||||
$model->created_at = time();
|
||||
$this->assertTrue($model->canResend());
|
||||
$this->assertEqualsWithDelta(Carbon::now(), $model->canResendAt(), 3);
|
||||
}
|
||||
|
||||
public function testIsStale() {
|
||||
$model = $this->createPartialMock(EmailActivation::class, ['getExpireDuration']);
|
||||
$model->method('getExpireDuration')->willReturn(new DateInterval('PT10M'));
|
||||
|
||||
$model->created_at = time();
|
||||
$this->assertFalse($model->isStale());
|
||||
|
||||
$model->created_at = time() - 60 * 10 - 1;
|
||||
$this->assertTrue($model->isStale());
|
||||
}
|
||||
|
||||
public function testIsStaleWithNullDuration() {
|
||||
$model = $this->createPartialMock(EmailActivation::class, ['getExpireDuration']);
|
||||
$model->method('getExpireDuration')->willReturn(null);
|
||||
|
||||
$model->created_at = time();
|
||||
$this->assertFalse($model->isStale());
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
use console\db\Migration;
|
||||
use yii\db\Expression;
|
||||
|
||||
class m191220_214310_rework_email_activations_data_column extends Migration {
|
||||
|
||||
public function safeUp() {
|
||||
$this->addColumn('email_activations', 'data', $this->json()->toString('data') . ' AFTER `_data`');
|
||||
$rows = $this->db->createCommand('
|
||||
SELECT `key`, `_data`
|
||||
FROM email_activations
|
||||
WHERE `_data` IS NOT NULL
|
||||
')->queryAll();
|
||||
foreach ($rows as $row) {
|
||||
$this->update('email_activations', [
|
||||
'data' => new Expression("'" . json_encode(unserialize($row['_data'])) . "'"),
|
||||
], [
|
||||
'key' => $row['key'],
|
||||
]);
|
||||
}
|
||||
|
||||
$this->dropColumn('email_activations', '_data');
|
||||
}
|
||||
|
||||
public function safeDown() {
|
||||
$this->addColumn('email_activations', '_data', $this->text()->after('type'));
|
||||
$rows = $this->db->createCommand('
|
||||
SELECT `key`, `data`
|
||||
FROM email_activations
|
||||
WHERE `data` IS NOT NULL
|
||||
')->queryAll();
|
||||
foreach ($rows as $row) {
|
||||
$this->update('email_activations', [
|
||||
'_data' => serialize(json_decode($row['data'], true)),
|
||||
], [
|
||||
'key' => $row['key'],
|
||||
]);
|
||||
}
|
||||
|
||||
$this->dropColumn('email_activations', 'data');
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user