Реализована форма смены ника пользователя

Добавлена базовая форма с запросом пароля
Валидация ника и email адреса вынесены из формы регистрации в модель аккаунта
Отрефакторен тест формы регистрации
Добавлены тесты для модели аккаунта
This commit is contained in:
ErickSkrauch 2016-03-20 02:25:26 +03:00
parent 8b1c3a477a
commit e67257b8aa
12 changed files with 431 additions and 55 deletions

View File

@ -2,6 +2,7 @@
namespace api\controllers;
use api\models\ChangePasswordForm;
use api\models\ChangeUsernameForm;
use common\models\Account;
use Yii;
use yii\filters\AccessControl;
@ -15,7 +16,7 @@ class AccountsController extends Controller {
'class' => AccessControl::class,
'rules' => [
[
'actions' => ['current', 'change-password'],
'actions' => ['current', 'change-password', 'change-username'],
'allow' => true,
'roles' => ['@'],
],
@ -61,4 +62,19 @@ class AccountsController extends Controller {
];
}
public function actionChangeUsername() {
$model = new ChangeUsernameForm();
$model->load(Yii::$app->request->post());
if (!$model->change()) {
return [
'success' => false,
'errors' => $this->normalizeModelErrors($model->getErrors()),
];
}
return [
'success' => true,
];
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace api\models;
use Yii;
class BasePasswordProtectedForm extends BaseApiForm {
public $password;
public function rules() {
return [
[['password'], 'required', 'message' => 'error.{attribute}_required'],
[['password'], 'validatePassword'],
];
}
public function validatePassword() {
if (!$this->getAccount()->validatePassword($this->password)) {
$this->addError('password', 'error.password_invalid');
}
}
/**
* @return \common\models\Account
*/
protected function getAccount() {
return Yii::$app->user->identity;
}
}

View File

@ -0,0 +1,38 @@
<?php
namespace api\models;
use common\models\Account;
use Yii;
use yii\helpers\ArrayHelper;
class ChangeUsernameForm extends BasePasswordProtectedForm {
public $username;
public function rules() {
return ArrayHelper::merge(parent::rules(), [
[['username'], 'required', 'message' => 'error.{attribute}_required'],
[['username'], 'validateUsername'],
]);
}
public function validateUsername($attribute) {
$account = new Account();
$account->username = $this->$attribute;
if (!$account->validate(['username'])) {
$account->addErrors($account->getErrors('username'));
}
}
public function change() {
if (!$this->validate()) {
return false;
}
$account = $this->getAccount();
$account->username = $this->username;
return $account->save();
}
}

View File

@ -19,23 +19,11 @@ class RegistrationForm extends BaseApiForm {
public function rules() {
return [
['rulesAgreement', 'required', 'message' => 'error.you_must_accept_rules'],
[[], ReCaptchaValidator::class, 'message' => 'error.captcha_invalid', 'when' => !YII_ENV_TEST],
['rulesAgreement', 'required', 'message' => 'error.you_must_accept_rules'],
['username', 'filter', 'filter' => 'trim'],
['username', 'required', 'message' => 'error.username_required'],
['username', 'string', 'min' => 3, 'max' => 21,
'tooShort' => 'error.username_too_short',
'tooLong' => 'error.username_too_long',
],
['username', 'match', 'pattern' => '/^[\p{L}\d-_\.!?#$%^&*()\[\]:;]+$/u'],
['username', 'unique', 'targetClass' => Account::class, 'message' => 'error.username_not_available'],
['email', 'filter', 'filter' => 'trim'],
['email', 'required', 'message' => 'error.email_required'],
['email', 'string', 'max' => 255, 'tooLong' => 'error.email_too_long'],
['email', 'email', 'checkDNS' => true, 'enableIDN' => true, 'message' => 'error.email_invalid'],
['email', 'unique', 'targetClass' => Account::class, 'message' => 'error.email_not_available'],
['username', 'validateUsername', 'skipOnEmpty' => false],
['email', 'validateEmail', 'skipOnEmpty' => false],
['password', 'required', 'message' => 'error.password_required'],
['rePassword', 'required', 'message' => 'error.rePassword_required'],
@ -44,6 +32,22 @@ class RegistrationForm extends BaseApiForm {
];
}
public function validateUsername() {
$account = new Account();
$account->username = $this->username;
if (!$account->validate(['username'])) {
$this->addErrors($account->getErrors());
}
}
public function validateEmail() {
$account = new Account();
$account->email = $this->email;
if (!$account->validate(['email'])) {
$this->addErrors($account->getErrors());
}
}
public function validatePasswordAndRePasswordMatch($attribute) {
if (!$this->hasErrors()) {
if ($this->password !== $this->rePassword) {

View File

@ -56,6 +56,22 @@ class Account extends ActiveRecord implements IdentityInterface {
public function rules() {
return [
[['username'], 'filter', 'filter' => 'trim'],
[['username'], 'required', 'message' => 'error.username_required'],
[['username'], 'string', 'min' => 3, 'max' => 21,
'tooShort' => 'error.username_too_short',
'tooLong' => 'error.username_too_long',
],
[['username'], 'match', 'pattern' => '/^[\p{L}\d-_\.!?#$%^&*()\[\]:;]+$/u',
'message' => 'error.username_invalid',
],
[['username'], 'unique', 'message' => 'error.username_not_available'],
[['email'], 'filter', 'filter' => 'trim'],
[['email'], 'required', 'message' => 'error.email_required'],
[['email'], 'string', 'max' => 255, 'tooLong' => 'error.email_too_long'],
[['email'], 'email', 'checkDNS' => true, 'enableIDN' => true, 'message' => 'error.email_invalid'],
[['email'], 'unique', 'message' => 'error.email_not_available'],
];
}

View File

@ -22,4 +22,12 @@ class AccountsRoute extends BasePage {
]);
}
public function changeUsername($currentPassword = null, $newUsername = null) {
$this->route = ['accounts/change-username'];
$this->actor->sendPOST($this->getUrl(), [
'password' => $currentPassword,
'username' => $newUsername,
]);
}
}

View File

@ -0,0 +1,43 @@
<?php
namespace tests\codeception\api\functional;
use Codeception\Scenario;
use Codeception\Specify;
use common\models\Account;
use tests\codeception\api\_pages\AccountsRoute;
use tests\codeception\api\_pages\LoginRoute;
use tests\codeception\api\functional\_steps\AccountSteps;
use tests\codeception\api\FunctionalTester;
class AccountsChangeUsernameCest {
/**
* @var AccountsRoute
*/
private $route;
public function _before(FunctionalTester $I) {
$this->route = new AccountsRoute($I);
}
public function _after(FunctionalTester $I) {
/** @var Account $account */
$account = Account::findOne(1);
$account->username = 'Admin';
$account->save();
}
public function testChangeUsername(FunctionalTester $I, Scenario $scenario) {
$I->wantTo('change my password');
$I = new AccountSteps($scenario);
$I->loggedInAsActiveAccount();
$this->route->changeUsername('password_0', 'bruce_wayne');
$I->canSeeResponseCodeIs(200);
$I->canSeeResponseIsJson();
$I->canSeeResponseContainsJson([
'success' => true,
]);
}
}

View File

@ -9,7 +9,7 @@ class BaseApiFormTest extends TestCase {
use Specify;
public function testLoad() {
$model = new DummyTestModel();
$model = new DummyBaseApiForm();
$this->specify('model should load data without ModelName array scope', function() use ($model) {
expect('model successful load data without prefix', $model->load(['field' => 'test-data']))->true();
expect('field is set as passed data', $model->field)->equals('test-data');
@ -18,7 +18,7 @@ class BaseApiFormTest extends TestCase {
}
class DummyTestModel extends BaseApiForm {
class DummyBaseApiForm extends BaseApiForm {
public $field;

View File

@ -0,0 +1,38 @@
<?php
namespace tests\codeception\api\models;
use api\models\BasePasswordProtectedForm;
use Codeception\Specify;
use common\models\Account;
use tests\codeception\api\unit\TestCase;
class BasePasswordProtectedFormTest extends TestCase {
use Specify;
public function testValidatePassword() {
$this->specify('error.password_invalid on passing invalid account password', function() {
$model = new DummyBasePasswordProtectedForm();
$model->password = 'some-invalid-password';
$model->validatePassword();
expect($model->getErrors('password'))->equals(['error.password_invalid']);
});
$this->specify('no errors on passing valid account password', function() {
$model = new DummyBasePasswordProtectedForm();
$model->password = 'password_0';
$model->validatePassword();
expect($model->getErrors('password'))->isEmpty();
});
}
}
class DummyBasePasswordProtectedForm extends BasePasswordProtectedForm {
protected function getAccount() {
return new Account([
'password' => 'password_0',
]);
}
}

View File

@ -0,0 +1,48 @@
<?php
namespace tests\codeception\api\models;
use api\models\ChangeUsernameForm;
use Codeception\Specify;
use common\models\Account;
use tests\codeception\api\unit\DbTestCase;
use tests\codeception\common\fixtures\AccountFixture;
use Yii;
/**
* @property array $accounts
*/
class ChangeUsernameFormTest extends DbTestCase {
use Specify;
public function fixtures() {
return [
'accounts' => [
'class' => AccountFixture::class,
'dataFile' => '@tests/codeception/common/fixtures/data/accounts.php',
],
];
}
public function testChange() {
$this->specify('successfully change username to new one', function() {
$model = new DummyChangeUsernameForm([
'password' => 'password_0',
'username' => 'my_new_nickname',
]);
expect($model->change())->true();
expect(Account::findOne(1)->username)->equals('my_new_nickname');
});
}
}
// TODO: тут образуется магическая переменная 1, что не круто. После перехода на php7 можно заюзать анонимный класс
// и создавать модель прямо внутри теста, где доступен объект фикстур с именами переменных
class DummyChangeUsernameForm extends ChangeUsernameForm {
protected function getAccount() {
return Account::findOne(1);
}
}

View File

@ -41,46 +41,27 @@ class RegistrationFormTest extends DbTestCase {
];
}
public function testNotCorrectRegistration() {
$model = new RegistrationForm([
'username' => 'valid_nickname',
'email' => 'correct-email@ely.by',
'password' => 'enough-length',
'rePassword' => 'password',
'rulesAgreement' => true,
]);
$this->specify('username and email in use, passwords not math - model is not created', function() use ($model) {
expect($model->signup())->null();
expect($model->getErrors())->notEmpty();
expect_file($this->getMessageFile())->notExists();
public function testValidatePasswordAndRePasswordMatch() {
$this->specify('error.rePassword_does_not_match if password and rePassword not match', function() {
$model = new RegistrationForm([
'password' => 'enough-length',
'rePassword' => 'password',
]);
expect($model->validate(['rePassword']))->false();
expect($model->getErrors('rePassword'))->equals(['error.rePassword_does_not_match']);
});
$this->specify('no errors if password and rePassword match', function() {
$model = new RegistrationForm([
'password' => 'enough-length',
'rePassword' => 'enough-length',
]);
expect($model->validate(['rePassword']))->true();
expect($model->getErrors('rePassword'))->isEmpty();
});
}
public function testUsernameValidators() {
$shouldBeValid = [
'русский_ник', 'русский_ник_на_грани!', 'numbers1132', '*__*-Stars-*__*', '1-_.!?#$%^&*()[]', '[ESP]Эрик',
'Свят_помидор;', роблена_ў_беларусі:)',
];
$shouldBeInvalid = [
'nick@name', 'spaced nick', ' ', 'sh', ' sh ',
];
foreach($shouldBeValid as $nickname) {
$model = new RegistrationForm([
'username' => $nickname,
]);
expect($nickname . ' passed validation', $model->validate(['username']))->true();
}
foreach($shouldBeInvalid as $nickname) {
$model = new RegistrationForm([
'username' => $nickname,
]);
expect($nickname . ' fail validation', $model->validate('username'))->false();
}
}
public function testCorrectSignup() {
public function testSignup() {
$model = new RegistrationForm([
'username' => 'some_username',
'email' => 'some_email@example.com',
@ -105,6 +86,8 @@ class RegistrationFormTest extends DbTestCase {
expect_file('message file exists', $this->getMessageFile())->exists();
}
// TODO: там в самой форме есть метод sendMail(), который рано или поздно должен переехать. К нему нужны будут тоже тесты
private function getMessageFile() {
/** @var \yii\swiftmailer\Mailer $mailer */
$mailer = Yii::$app->mailer;

View File

@ -0,0 +1,152 @@
<?php
namespace tests\codeception\common\unit\models;
use Codeception\Specify;
use common\components\UserPass;
use common\models\Account;
use tests\codeception\common\fixtures\AccountFixture;
use tests\codeception\common\unit\DbTestCase;
use Yii;
/**
* @property array $accounts
*/
class AccountTest extends DbTestCase {
use Specify;
public function fixtures() {
return [
'accounts' => [
'class' => AccountFixture::class,
'dataFile' => '@tests/codeception/common/fixtures/data/accounts.php',
],
];
}
public function testValidateUsername() {
$this->specify('username required', function() {
$model = new Account(['username' => null]);
expect($model->validate(['username']))->false();
expect($model->getErrors('username'))->equals(['error.username_required']);
});
$this->specify('username should be at least 3 symbols length', function() {
$model = new Account(['username' => 'at']);
expect($model->validate(['username']))->false();
expect($model->getErrors('username'))->equals(['error.username_too_short']);
});
$this->specify('username should be not more than 21 symbols length', function() {
$model = new Account(['username' => 'erickskrauch_erickskrauch']);
expect($model->validate(['username']))->false();
expect($model->getErrors('username'))->equals(['error.username_too_long']);
});
$this->specify('username can contain many cool symbols', function() {
$shouldBeValid = [
'русский_ник', 'русский_ник_на_грани!', 'numbers1132', '*__*-Stars-*__*', '1-_.!?#$%^&*()[]',
'[ESP]Эрик', 'Свят_помидор;', роблена_ў_беларусі:)',
];
foreach($shouldBeValid as $nickname) {
$model = new Account(['username' => $nickname]);
expect($nickname . ' passed validation', $model->validate(['username']))->true();
expect($model->getErrors('username'))->isEmpty();
}
});
$this->specify('username cannot contain some symbols', function() {
$shouldBeInvalid = [
'nick@name', 'spaced nick',
];
foreach($shouldBeInvalid as $nickname) {
$model = new Account(['username' => $nickname]);
expect($nickname . ' fail validation', $model->validate('username'))->false();
expect($model->getErrors('username'))->equals(['error.username_invalid']);
}
});
$this->specify('username should be unique', function() {
$model = new Account(['username' => $this->accounts['admin']['username']]);
expect($model->validate('username'))->false();
expect($model->getErrors('username'))->equals(['error.username_not_available']);
});
}
public function testValidateEmail() {
$this->specify('email required', function() {
$model = new Account(['email' => null]);
expect($model->validate(['email']))->false();
expect($model->getErrors('email'))->equals(['error.email_required']);
});
$this->specify('email should be not more 255 symbols (I hope it\'s impossible to register)', function() {
$model = new Account([
'email' => 'emailemailemailemailemailemailemailemailemailemailemailemailemailemailemailemailemail' .
'emailemailemailemailemailemailemailemailemailemailemailemailemailemailemailemailemail' .
'emailemailemailemailemailemailemailemailemailemailemailemailemailemailemailemailemail' .
'emailemail', // = 256 symbols
]);
expect($model->validate(['email']))->false();
expect($model->getErrors('email'))->equals(['error.email_too_long']);
});
$this->specify('email should be email (it test can fail, if you don\'t have internet connection)', function() {
$model = new Account(['email' => 'invalid_email']);
expect($model->validate(['email']))->false();
expect($model->getErrors('email'))->equals(['error.email_invalid']);
});
$this->specify('email should be unique', function() {
$model = new Account(['email' => $this->accounts['admin']['email']]);
expect($model->validate('email'))->false();
expect($model->getErrors('email'))->equals(['error.email_not_available']);
});
}
public function testSetPassword() {
$this->specify('calling method should change password and set latest password hash algorithm', function() {
$model = new Account();
$model->setPassword('12345678');
expect('hash should be set', $model->password_hash)->notEmpty();
expect('validation should be passed', $model->validatePassword('12345678'))->true();
expect('latest password hash should be used', $model->password_hash_strategy)->equals(Account::PASS_HASH_STRATEGY_YII2);
});
}
public function testValidatePassword() {
$this->specify('old Ely password should work', function() {
$model = new Account([
'email' => 'erick@skrauch.net',
'password_hash' => UserPass::make('erick@skrauch.net', '12345678'),
]);
expect('valid password should pass', $model->validatePassword('12345678', Account::PASS_HASH_STRATEGY_OLD_ELY))->true();
expect('invalid password should fail', $model->validatePassword('87654321', Account::PASS_HASH_STRATEGY_OLD_ELY))->false();
});
$this->specify('modern hash algorithm should work', function() {
$model = new Account([
'password_hash' => Yii::$app->security->generatePasswordHash('12345678'),
]);
expect('valid password should pass', $model->validatePassword('12345678', Account::PASS_HASH_STRATEGY_YII2))->true();
expect('invalid password should fail', $model->validatePassword('87654321', Account::PASS_HASH_STRATEGY_YII2))->false();
});
$this->specify('if second argument is not pass model value should be used', function() {
$model = new Account([
'email' => 'erick@skrauch.net',
'password_hash_strategy' => Account::PASS_HASH_STRATEGY_OLD_ELY,
'password_hash' => UserPass::make('erick@skrauch.net', '12345678'),
]);
expect('valid password should pass', $model->validatePassword('12345678'))->true();
expect('invalid password should fail', $model->validatePassword('87654321'))->false();
$model = new Account([
'password_hash_strategy' => Account::PASS_HASH_STRATEGY_YII2,
'password_hash' => Yii::$app->security->generatePasswordHash('12345678'),
]);
expect('valid password should pass', $model->validatePassword('12345678'))->true();
expect('invalid password should fail', $model->validatePassword('87654321'))->false();
});
}
}