Rework email_activation model, get rid of behaviors, use json column to store additional data

This commit is contained in:
ErickSkrauch 2019-12-21 01:23:58 +03:00
parent 22e8158581
commit 666213afc7
26 changed files with 254 additions and 454 deletions

View File

@ -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(),
],
];

View File

@ -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(),
];
}

View File

@ -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;

View File

@ -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)

View File

@ -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(),
];
}

View File

@ -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()

View File

@ -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');
}

View File

@ -53,7 +53,6 @@ class RepeatAccountActivationCest {
],
]);
$I->canSeeResponseJsonMatchesJsonPath('$.data.canRepeatIn');
$I->canSeeResponseJsonMatchesJsonPath('$.data.repeatFrequency');
}
public function testSuccess(FunctionalTester $I) {

View File

@ -39,7 +39,6 @@ class ChangeEmailInitializeCest {
],
]);
$I->canSeeResponseJsonMatchesJsonPath('$.data.canRepeatIn');
$I->canSeeResponseJsonMatchesJsonPath('$.data.repeatFrequency');
}
}

View File

@ -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]);
}
};

View File

@ -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);
}
};

View File

@ -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);
}

View File

@ -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();

View File

@ -37,7 +37,7 @@ class EmailActivationKeyValidator extends Validator {
return;
}
if ($activation->isExpired()) {
if ($activation->isStale()) {
$this->addError($model, $attribute, $this->expired);
return;
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
@ -14,19 +14,17 @@ use yii\helpers\ArrayHelper;
/**
* Fields:
* @property string $key
* @property integer $account_id
* @property integer $type
* @property string $_data
* @property integer $created_at
* @property string $key
* @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);
}
}

View File

@ -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');
}
}

View File

@ -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');
}
}

View File

@ -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,
]);
}
}

View File

@ -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);
}
}

View File

@ -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,
],
];

View File

@ -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,
],
];
}
};
}
}

View File

@ -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;
}
}

View File

@ -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());
}
}

View File

@ -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');
}
}