Объединены сущности для авторизации посредством JWT токенов и токенов, выданных через oAuth2.

Все действия, связанные с аккаунтами, теперь вызываются через url `/api/v1/accounts/<id>/<action>`.
Добавлена вменяемая система разграничения прав на основе RBAC.
Теперь oAuth2 токены генерируются как случайная строка в 40 символов длинной, а не UUID.
Исправлен баг с неправильным временем жизни токена в ответе успешного запроса аутентификации.
Теперь все unit тесты можно успешно прогнать без наличия интернета.
This commit is contained in:
ErickSkrauch
2017-09-19 20:06:16 +03:00
parent 928b3aa7fc
commit dd2c4bc413
173 changed files with 2719 additions and 2748 deletions

View File

@ -0,0 +1,22 @@
<?php
namespace tests\codeception\api\unit\modules\accounts\models;
use api\modules\accounts\models\AcceptRulesForm;
use common\models\Account;
use tests\codeception\api\unit\TestCase;
use const common\LATEST_RULES_VERSION;
class AcceptRulesFormTest extends TestCase {
public function testAgreeWithLatestRules() {
/** @var Account|\Mockery\MockInterface $account */
$account = mock(Account::class . '[save]');
$account->shouldReceive('save')->andReturn(true);
$account->rules_agreement_version = LATEST_RULES_VERSION - 1;
$model = new AcceptRulesForm($account);
$this->assertTrue($model->performAction());
$this->assertEquals(LATEST_RULES_VERSION, $account->rules_agreement_version);
}
}

View File

@ -0,0 +1,54 @@
<?php
namespace tests\codeception\api\unit\modules\accounts\models;
use api\modules\accounts\models\ChangeEmailForm;
use common\models\Account;
use common\models\EmailActivation;
use tests\codeception\api\unit\TestCase;
use tests\codeception\common\fixtures\AccountFixture;
use tests\codeception\common\fixtures\EmailActivationFixture;
class ChangeEmailFormTest extends TestCase {
public function _fixtures() {
return [
'accounts' => AccountFixture::class,
'emailActivations' => EmailActivationFixture::class,
];
}
public function testChangeEmail() {
/** @var Account $account */
$account = Account::findOne($this->getAccountId());
$newEmailConfirmationFixture = $this->tester->grabFixture('emailActivations', 'newEmailConfirmation');
$model = new ChangeEmailForm($account, [
'key' => $newEmailConfirmationFixture['key'],
]);
$this->assertTrue($model->performAction());
$this->assertNull(EmailActivation::findOne([
'account_id' => $account->id,
'type' => EmailActivation::TYPE_NEW_EMAIL_CONFIRMATION,
]));
/** @noinspection UnserializeExploitsInspection */
$data = unserialize($newEmailConfirmationFixture['_data']);
$this->assertEquals($data['newEmail'], $account->email);
$this->tester->canSeeAmqpMessageIsCreated('events');
}
public function testCreateTask() {
/** @var Account $account */
$account = Account::findOne($this->getAccountId());
$model = new ChangeEmailForm($account);
$model->createTask(1, 'test1@ely.by', 'test@ely.by');
$message = $this->tester->grabLastSentAmqpMessage('events');
$body = json_decode($message->getBody(), true);
$this->assertEquals(1, $body['accountId']);
$this->assertEquals('test1@ely.by', $body['newEmail']);
$this->assertEquals('test@ely.by', $body['oldEmail']);
}
private function getAccountId() {
return $this->tester->grabFixture('accounts', 'account-with-change-email-finish-state')['id'];
}
}

View File

@ -0,0 +1,21 @@
<?php
namespace tests\codeception\api\unit\modules\accounts\models;
use api\modules\accounts\models\ChangeLanguageForm;
use common\models\Account;
use tests\codeception\api\unit\TestCase;
class ChangeLanguageFormTest extends TestCase {
public function testApplyLanguage() {
/** @var Account|\Mockery\MockInterface $account */
$account = mock(Account::class . '[save]');
$account->shouldReceive('save')->andReturn(true);
$model = new ChangeLanguageForm($account);
$model->lang = 'ru';
$this->assertTrue($model->performAction());
$this->assertEquals('ru', $account->lang);
}
}

View File

@ -0,0 +1,139 @@
<?php
namespace tests\codeception\api\unit\modules\accounts\models;
use api\components\User\Component;
use api\components\User\Identity;
use api\modules\accounts\models\ChangePasswordForm;
use common\components\UserPass;
use common\helpers\Error as E;
use common\models\Account;
use tests\codeception\api\unit\TestCase;
use Yii;
use yii\db\Transaction;
class ChangePasswordFormTest extends TestCase {
public function testValidatePasswordAndRePasswordMatch() {
$account = new Account();
$account->setPassword('12345678');
$model = new ChangePasswordForm($account, [
'password' => '12345678',
'newPassword' => 'my-new-password',
'newRePassword' => 'another-password',
]);
$model->validatePasswordAndRePasswordMatch('newRePassword');
$this->assertEquals(
[E::NEW_RE_PASSWORD_DOES_NOT_MATCH],
$model->getErrors('newRePassword'),
'error.rePassword_does_not_match expected if passwords not match'
);
$account = new Account();
$account->setPassword('12345678');
$model = new ChangePasswordForm($account, [
'password' => '12345678',
'newPassword' => 'my-new-password',
'newRePassword' => 'my-new-password',
]);
$model->validatePasswordAndRePasswordMatch('newRePassword');
$this->assertEmpty($model->getErrors('newRePassword'), 'no errors expected if passwords are valid');
// this is very important, because password change flow may be combined of two steps
// therefore we need to validate password sameness before we will validate current account password
$account = new Account();
$account->setPassword('12345678');
$model = new ChangePasswordForm($account, [
'newPassword' => 'my-new-password',
'newRePassword' => 'another-password',
]);
$model->validate();
$this->assertEquals(
[E::NEW_RE_PASSWORD_DOES_NOT_MATCH],
$model->getErrors('newRePassword'),
'error.rePassword_does_not_match expected even if there are errors on other attributes'
);
$this->assertEmpty($model->getErrors('password'));
}
public function testPerformAction() {
$component = mock(Component::class . '[terminateSessions]', [[
'identityClass' => Identity::class,
'enableSession' => false,
'loginUrl' => null,
'secret' => 'secret',
]]);
$component->shouldNotReceive('terminateSessions');
Yii::$app->set('user', $component);
$transaction = mock(Transaction::class . '[commit]');
$transaction->shouldReceive('commit');
$connection = mock(Yii::$app->db);
$connection->shouldReceive('beginTransaction')->andReturn($transaction);
Yii::$app->set('db', $connection);
/** @var Account|\Mockery\MockInterface $account */
$account = mock(Account::class . '[save]');
$account->shouldReceive('save')->andReturn(true);
$account->setPassword('password_0');
$model = new ChangePasswordForm($account, [
'password' => 'password_0',
'newPassword' => 'my-new-password',
'newRePassword' => 'my-new-password',
]);
$callTime = time();
$this->assertTrue($model->performAction(), 'successfully change password with modern hash strategy');
$this->assertTrue($account->validatePassword('my-new-password'), 'new password should be successfully stored into account');
$this->assertGreaterThanOrEqual($callTime, $account->password_changed_at, 'password change time updated');
/** @var Account|\Mockery\MockInterface $account */
$account = mock(Account::class . '[save]');
$account->shouldReceive('save')->andReturn(true);
$account->email = 'mock@ely.by';
$account->password_hash_strategy = Account::PASS_HASH_STRATEGY_OLD_ELY;
$account->password_hash = UserPass::make($account->email, '12345678');
$model = new ChangePasswordForm($account, [
'password' => '12345678',
'newPassword' => 'my-new-password',
'newRePassword' => 'my-new-password',
]);
$callTime = time();
$this->assertTrue($model->performAction(), 'successfully change password with legacy hash strategy');
$this->assertTrue($account->validatePassword('my-new-password'));
$this->assertGreaterThanOrEqual($callTime, $account->password_changed_at);
$this->assertEquals(Account::PASS_HASH_STRATEGY_YII2, $account->password_hash_strategy);
}
public function testPerformActionWithLogout() {
/** @var Account|\Mockery\MockInterface $account */
$account = mock(Account::class . '[save]');
$account->shouldReceive('save')->andReturn(true);
$account->setPassword('password_0');
/** @var Component|\Mockery\MockInterface $component */
$component = mock(Component::class . '[terminateSessions]', [[
'identityClass' => Identity::class,
'enableSession' => false,
'loginUrl' => null,
'secret' => 'secret',
]]);
$component->shouldReceive('terminateSessions')->once()->withArgs([$account, Component::KEEP_CURRENT_SESSION]);
Yii::$app->set('user', $component);
$model = new ChangePasswordForm($account, [
'password' => 'password_0',
'newPassword' => 'my-new-password',
'newRePassword' => 'my-new-password',
'logoutAll' => true,
]);
$this->assertTrue($model->performAction());
}
}

View File

@ -0,0 +1,82 @@
<?php
namespace tests\codeception\api\unit\modules\accounts\models;
use api\modules\accounts\models\ChangeUsernameForm;
use common\models\Account;
use common\models\UsernameHistory;
use tests\codeception\api\unit\TestCase;
use tests\codeception\common\fixtures\AccountFixture;
use tests\codeception\common\fixtures\UsernameHistoryFixture;
class ChangeUsernameFormTest extends TestCase {
public function _fixtures() {
return [
'accounts' => AccountFixture::class,
'history' => UsernameHistoryFixture::class,
];
}
public function testPerformAction() {
$model = new ChangeUsernameForm($this->getAccount(), [
'password' => 'password_0',
'username' => 'my_new_nickname',
]);
$this->assertTrue($model->performAction());
$this->assertEquals('my_new_nickname', Account::findOne($this->getAccountId())->username);
$this->assertInstanceOf(UsernameHistory::class, UsernameHistory::findOne(['username' => 'my_new_nickname']));
$this->tester->canSeeAmqpMessageIsCreated('events');
}
public function testPerformActionWithTheSameUsername() {
$account = $this->getAccount();
$username = $account->username;
$model = new ChangeUsernameForm($account, [
'password' => 'password_0',
'username' => $username,
]);
$callTime = time();
$this->assertTrue($model->performAction());
$this->assertNull(UsernameHistory::findOne([
'AND',
'username' => $username,
['>=', 'applied_in', $callTime],
]), 'no new UsernameHistory record, if we don\'t change username');
$this->tester->cantSeeAmqpMessageIsCreated('events');
}
public function testPerformActionWithChangeCase() {
$newUsername = mb_strtoupper($this->tester->grabFixture('accounts', 'admin')['username']);
$model = new ChangeUsernameForm($this->getAccount(), [
'password' => 'password_0',
'username' => $newUsername,
]);
$this->assertTrue($model->performAction());
$this->assertEquals($newUsername, Account::findOne($this->getAccountId())->username);
$this->assertInstanceOf(
UsernameHistory::class,
UsernameHistory::findOne(['username' => $newUsername]),
'username should change, if we change case of some letters'
);
$this->tester->canSeeAmqpMessageIsCreated('events');
}
public function testCreateTask() {
$model = new ChangeUsernameForm($this->getAccount());
$model->createEventTask(1, 'test1', 'test');
$message = $this->tester->grabLastSentAmqpMessage('events');
$body = json_decode($message->getBody(), true);
$this->assertEquals(1, $body['accountId']);
$this->assertEquals('test1', $body['newUsername']);
$this->assertEquals('test', $body['oldUsername']);
}
private function getAccount(): Account {
return $this->tester->grabFixture('accounts', 'admin');
}
private function getAccountId() {
return $this->getAccount()->id;
}
}

View File

@ -0,0 +1,42 @@
<?php
namespace tests\codeception\api\unit\modules\accounts\models;
use api\modules\accounts\models\DisableTwoFactorAuthForm;
use common\helpers\Error as E;
use common\models\Account;
use tests\codeception\api\unit\TestCase;
class DisableTwoFactorAuthFormTest extends TestCase {
public function testPerformAction() {
/** @var Account|\Mockery\MockInterface $account */
$account = mock(Account::class)->makePartial();
$account->shouldReceive('save')->once()->andReturn(true);
$account->is_otp_enabled = true;
$account->otp_secret = 'mock secret';
/** @var DisableTwoFactorAuthForm|\Mockery\MockInterface $model */
$model = mock(DisableTwoFactorAuthForm::class . '[validate]', [$account]);
$model->shouldReceive('validate')->once()->andReturn(true);
$this->assertTrue($model->performAction());
$this->assertNull($account->otp_secret);
$this->assertFalse($account->is_otp_enabled);
}
public function testValidateOtpEnabled() {
$account = new Account();
$account->is_otp_enabled = false;
$model = new DisableTwoFactorAuthForm($account);
$model->validateOtpEnabled('account');
$this->assertEquals([E::OTP_NOT_ENABLED], $model->getErrors('account'));
$account = new Account();
$account->is_otp_enabled = true;
$model = new DisableTwoFactorAuthForm($account);
$model->validateOtpEnabled('account');
$this->assertEmpty($model->getErrors('account'));
}
}

View File

@ -0,0 +1,54 @@
<?php
namespace tests\codeception\api\unit\modules\accounts\models;
use api\components\User\Component;
use api\components\User\Identity;
use api\modules\accounts\models\EnableTwoFactorAuthForm;
use common\helpers\Error as E;
use common\models\Account;
use tests\codeception\api\unit\TestCase;
use Yii;
class EnableTwoFactorAuthFormTest extends TestCase {
public function testPerformAction() {
/** @var Account|\Mockery\MockInterface $account */
$account = mock(Account::class . '[save]');
$account->shouldReceive('save')->andReturn(true);
$account->is_otp_enabled = false;
$account->otp_secret = 'mock secret';
/** @var Component|\Mockery\MockInterface $component */
$component = mock(Component::class . '[terminateSessions]', [[
'identityClass' => Identity::class,
'enableSession' => false,
'loginUrl' => null,
'secret' => 'secret',
]]);
$component->shouldReceive('terminateSessions')->withArgs([$account, Component::KEEP_CURRENT_SESSION]);
Yii::$app->set('user', $component);
/** @var EnableTwoFactorAuthForm|\Mockery\MockInterface $model */
$model = mock(EnableTwoFactorAuthForm::class . '[validate]', [$account]);
$model->shouldReceive('validate')->andReturn(true);
$this->assertTrue($model->performAction());
$this->assertTrue($account->is_otp_enabled);
}
public function testValidateOtpDisabled() {
$account = new Account();
$account->is_otp_enabled = true;
$model = new EnableTwoFactorAuthForm($account);
$model->validateOtpDisabled('account');
$this->assertEquals([E::OTP_ALREADY_ENABLED], $model->getErrors('account'));
$account = new Account();
$account->is_otp_enabled = false;
$model = new EnableTwoFactorAuthForm($account);
$model->validateOtpDisabled('account');
$this->assertEmpty($model->getErrors('account'));
}
}

View File

@ -0,0 +1,45 @@
<?php
namespace tests\codeception\api\unit\modules\accounts\models;
use api\modules\accounts\models\SendEmailVerificationForm;
use common\models\Account;
use common\models\confirmations\CurrentEmailConfirmation;
use common\models\EmailActivation;
use tests\codeception\api\unit\TestCase;
use tests\codeception\common\fixtures\AccountFixture;
use tests\codeception\common\fixtures\EmailActivationFixture;
class SendEmailVerificationFormTest extends TestCase {
public function _fixtures() {
return [
'accounts' => AccountFixture::class,
'emailActivations' => EmailActivationFixture::class,
];
}
public function testCreateCode() {
/** @var Account $account */
$account = $this->tester->grabFixture('accounts', 'admin');
$model = new SendEmailVerificationForm($account);
$activationModel = $model->createCode();
$this->assertInstanceOf(CurrentEmailConfirmation::class, $activationModel);
$this->assertEquals($account->id, $activationModel->account_id);
$this->assertNotNull(EmailActivation::findOne($activationModel->key));
}
public function testSendCurrentEmailConfirmation() {
/** @var Account $account */
$account = $this->tester->grabFixture('accounts', 'admin');
$model = new SendEmailVerificationForm($account, [
'password' => 'password_0',
]);
$this->assertTrue($model->performAction());
$this->assertTrue(EmailActivation::find()->andWhere([
'account_id' => $account->id,
'type' => EmailActivation::TYPE_CURRENT_EMAIL_CONFIRMATION,
])->exists());
$this->tester->canSeeEmailIsSent();
}
}

View File

@ -0,0 +1,54 @@
<?php
namespace tests\codeception\api\unit\modules\accounts\models;
use api\modules\accounts\models\SendNewEmailVerificationForm;
use common\models\Account;
use common\models\confirmations\NewEmailConfirmation;
use common\models\EmailActivation;
use tests\codeception\api\unit\TestCase;
use tests\codeception\common\fixtures\AccountFixture;
use tests\codeception\common\fixtures\EmailActivationFixture;
use tests\codeception\common\helpers\Mock;
use yii\validators\EmailValidator;
class SendNewEmailVerificationFormTest extends TestCase {
public function _fixtures() {
return [
'accounts' => AccountFixture::class,
'emailActivations' => EmailActivationFixture::class,
];
}
public function testCreateCode() {
/** @var Account $account */
$account = $this->tester->grabFixture('accounts', 'admin');
$model = new SendNewEmailVerificationForm($account);
$model->email = 'my-new-email@ely.by';
$activationModel = $model->createCode();
$this->assertInstanceOf(NewEmailConfirmation::class, $activationModel);
$this->assertEquals($account->id, $activationModel->account_id);
$this->assertEquals($model->email, $activationModel->newEmail);
$this->assertNotNull(EmailActivation::findOne($activationModel->key));
}
public function testSendNewEmailConfirmation() {
/** @var Account $account */
$account = $this->tester->grabFixture('accounts', 'account-with-change-email-init-state');
/** @var SendNewEmailVerificationForm $model */
$key = $this->tester->grabFixture('emailActivations', 'currentChangeEmailConfirmation')['key'];
$model = new SendNewEmailVerificationForm($account, [
'key' => $key,
'email' => 'my-new-email@ely.by',
]);
Mock::func(EmailValidator::class, 'checkdnsrr')->andReturn(true);
$this->assertTrue($model->performAction());
$this->assertNull(EmailActivation::findOne($key));
$this->assertNotNull(EmailActivation::findOne([
'account_id' => $account->id,
'type' => EmailActivation::TYPE_NEW_EMAIL_CONFIRMATION,
]));
$this->tester->canSeeEmailIsSent();
}
}

View File

@ -0,0 +1,47 @@
<?php
namespace tests\codeception\api\unit\modules\accounts\models;
use api\modules\accounts\models\TwoFactorAuthInfo;
use common\models\Account;
use tests\codeception\api\unit\TestCase;
class TwoFactorAuthInfoTest extends TestCase {
public function testGetCredentials() {
/** @var Account|\Mockery\MockInterface $account */
$account = mock(Account::class . '[save]');
$account->shouldReceive('save')->andReturn(true);
$account->email = 'mock@email.com';
$account->otp_secret = null;
$model = new TwoFactorAuthInfo($account);
$result = $model->getCredentials();
$this->assertTrue(is_array($result));
$this->assertArrayHasKey('qr', $result);
$this->assertArrayHasKey('uri', $result);
$this->assertArrayHasKey('secret', $result);
$this->assertSame($account->otp_secret, $result['secret']);
$this->assertSame(strtoupper($account->otp_secret), $account->otp_secret);
$this->assertStringStartsWith('data:image/svg+xml,<?xml', $result['qr']);
$previous = libxml_use_internal_errors(true);
simplexml_load_string(base64_decode($result['qr']));
libxml_use_internal_errors($previous);
$this->assertEmpty(libxml_get_errors());
/** @var Account|\Mockery\MockInterface $account */
$account = mock(Account::class . '[save]');
$account->shouldReceive('save')->andReturn(true);
$account->email = 'mock@email.com';
$account->otp_secret = 'AAAA';
$model = new TwoFactorAuthInfo($account);
$result = $model->getCredentials();
$this->assertEquals('AAAA', $result['secret']);
}
}

View File

@ -1,7 +1,6 @@
<?php
namespace codeception\api\unit\modules\authserver\models;
use api\models\AccountIdentity;
use api\models\authentication\LoginForm;
use api\modules\authserver\models\AuthenticateData;
use api\modules\authserver\models\AuthenticationForm;
@ -117,7 +116,7 @@ class AuthenticationFormTest extends TestCase {
->setMethods(['getAccount'])
->getMock();
$account = new AccountIdentity();
$account = new Account();
$account->username = 'dummy';
$account->email = 'dummy@ely.by';
$account->status = $status;

View File

@ -2,7 +2,7 @@
namespace tests\codeception\api\unit\modules\internal\models;
use api\modules\internal\helpers\Error as E;
use api\modules\internal\models\BanForm;
use api\modules\accounts\models\BanAccountForm;
use common\models\Account;
use tests\codeception\api\unit\TestCase;
@ -11,13 +11,13 @@ class BanFormTest extends TestCase {
public function testValidateAccountActivity() {
$account = new Account();
$account->status = Account::STATUS_ACTIVE;
$form = new BanForm($account);
$form = new BanAccountForm($account);
$form->validateAccountActivity();
$this->assertEmpty($form->getErrors('account'));
$account = new Account();
$account->status = Account::STATUS_BANNED;
$form = new BanForm($account);
$form = new BanAccountForm($account);
$form->validateAccountActivity();
$this->assertEquals([E::ACCOUNT_ALREADY_BANNED], $form->getErrors('account'));
}
@ -32,8 +32,8 @@ class BanFormTest extends TestCase {
->method('save')
->willReturn(true);
$model = new BanForm($account);
$this->assertTrue($model->ban());
$model = new BanAccountForm($account);
$this->assertTrue($model->performAction());
$this->assertEquals(Account::STATUS_BANNED, $account->status);
$this->tester->canSeeAmqpMessageIsCreated('events');
}
@ -42,14 +42,14 @@ class BanFormTest extends TestCase {
$account = new Account();
$account->id = 3;
$model = new BanForm($account);
$model = new BanAccountForm($account);
$model->createTask();
$message = json_decode($this->tester->grabLastSentAmqpMessage('events')->body, true);
$this->assertSame(3, $message['accountId']);
$this->assertSame(-1, $message['duration']);
$this->assertSame('', $message['message']);
$model = new BanForm($account);
$model = new BanAccountForm($account);
$model->duration = 123;
$model->message = 'test';
$model->createTask();

View File

@ -2,7 +2,7 @@
namespace tests\codeception\api\unit\modules\internal\models;
use api\modules\internal\helpers\Error as E;
use api\modules\internal\models\PardonForm;
use api\modules\accounts\models\PardonAccountForm;
use common\models\Account;
use tests\codeception\api\unit\TestCase;
@ -11,13 +11,13 @@ class PardonFormTest extends TestCase {
public function testValidateAccountBanned() {
$account = new Account();
$account->status = Account::STATUS_BANNED;
$form = new PardonForm($account);
$form = new PardonAccountForm($account);
$form->validateAccountBanned();
$this->assertEmpty($form->getErrors('account'));
$account = new Account();
$account->status = Account::STATUS_ACTIVE;
$form = new PardonForm($account);
$form = new PardonAccountForm($account);
$form->validateAccountBanned();
$this->assertEquals([E::ACCOUNT_NOT_BANNED], $form->getErrors('account'));
}
@ -33,8 +33,8 @@ class PardonFormTest extends TestCase {
->willReturn(true);
$account->status = Account::STATUS_BANNED;
$model = new PardonForm($account);
$this->assertTrue($model->pardon());
$model = new PardonAccountForm($account);
$this->assertTrue($model->performAction());
$this->assertEquals(Account::STATUS_ACTIVE, $account->status);
$this->tester->canSeeAmqpMessageIsCreated('events');
}
@ -43,7 +43,7 @@ class PardonFormTest extends TestCase {
$account = new Account();
$account->id = 3;
$model = new PardonForm($account);
$model = new PardonAccountForm($account);
$model->createTask();
$message = json_decode($this->tester->grabLastSentAmqpMessage('events')->body, true);
$this->assertSame(3, $message['accountId']);