mirror of
https://github.com/elyby/accounts.git
synced 2025-05-31 14:11:46 +05:30
Extract login logics into a separate component. Not quite clean result but enough for upcoming tasks
This commit is contained in:
@@ -1,122 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api\tests\unit\models\authentication;
|
||||
|
||||
use api\models\authentication\LoginForm;
|
||||
use api\tests\unit\TestCase;
|
||||
use common\models\Account;
|
||||
use common\tests\fixtures\AccountFixture;
|
||||
use OTPHP\TOTP;
|
||||
|
||||
class LoginFormTest extends TestCase {
|
||||
|
||||
public function _fixtures(): array {
|
||||
return [
|
||||
'accounts' => AccountFixture::class,
|
||||
];
|
||||
}
|
||||
|
||||
public function testValidateLogin(): void {
|
||||
$model = $this->createWithAccount(null);
|
||||
$model->login = 'mock-login';
|
||||
$model->validateLogin('login');
|
||||
$this->assertSame(['error.login_not_exist'], $model->getErrors('login'));
|
||||
|
||||
$model = $this->createWithAccount(new Account());
|
||||
$model->login = 'mock-login';
|
||||
$model->validateLogin('login');
|
||||
$this->assertEmpty($model->getErrors('login'));
|
||||
}
|
||||
|
||||
public function testValidatePassword(): void {
|
||||
$account = new Account();
|
||||
$account->password_hash = '$2y$04$N0q8DaHzlYILCnLYrpZfEeWKEqkPZzbawiS07GbSr/.xbRNweSLU6'; // 12345678
|
||||
$account->password_hash_strategy = Account::PASS_HASH_STRATEGY_YII2;
|
||||
|
||||
$model = $this->createWithAccount($account);
|
||||
$model->password = '87654321';
|
||||
$model->validatePassword('password');
|
||||
$this->assertSame(['error.password_incorrect'], $model->getErrors('password'));
|
||||
|
||||
$model = $this->createWithAccount($account);
|
||||
$model->password = '12345678';
|
||||
$model->validatePassword('password');
|
||||
$this->assertEmpty($model->getErrors('password'));
|
||||
}
|
||||
|
||||
public function testValidateTotp(): void {
|
||||
$account = new Account(['password' => '12345678']);
|
||||
$account->password = '12345678';
|
||||
$account->is_otp_enabled = true;
|
||||
$account->otp_secret = 'AAAA';
|
||||
|
||||
$model = $this->createWithAccount($account);
|
||||
$model->password = '12345678';
|
||||
$model->totp = '321123';
|
||||
$model->validateTotp('totp');
|
||||
$this->assertSame(['error.totp_incorrect'], $model->getErrors('totp'));
|
||||
|
||||
$totp = TOTP::create($account->otp_secret);
|
||||
$model = $this->createWithAccount($account);
|
||||
$model->password = '12345678';
|
||||
$model->totp = $totp->now();
|
||||
$model->validateTotp('totp');
|
||||
$this->assertEmpty($model->getErrors('totp'));
|
||||
}
|
||||
|
||||
public function testValidateActivity(): void {
|
||||
$account = new Account();
|
||||
$account->status = Account::STATUS_REGISTERED;
|
||||
$model = $this->createWithAccount($account);
|
||||
$model->validateActivity('login');
|
||||
$this->assertSame(['error.account_not_activated'], $model->getErrors('login'));
|
||||
|
||||
$account = new Account();
|
||||
$account->status = Account::STATUS_BANNED;
|
||||
$model = $this->createWithAccount($account);
|
||||
$model->validateActivity('login');
|
||||
$this->assertSame(['error.account_banned'], $model->getErrors('login'));
|
||||
|
||||
$account = new Account();
|
||||
$account->status = Account::STATUS_ACTIVE;
|
||||
$model = $this->createWithAccount($account);
|
||||
$model->validateActivity('login');
|
||||
$this->assertEmpty($model->getErrors('login'));
|
||||
}
|
||||
|
||||
public function testLogin(): void {
|
||||
$account = new Account();
|
||||
$account->id = 1;
|
||||
$account->username = 'erickskrauch';
|
||||
$account->password_hash = '$2y$04$N0q8DaHzlYILCnLYrpZfEeWKEqkPZzbawiS07GbSr/.xbRNweSLU6'; // 12345678
|
||||
$account->password_hash_strategy = Account::PASS_HASH_STRATEGY_YII2;
|
||||
$account->status = Account::STATUS_ACTIVE;
|
||||
|
||||
$model = $this->createWithAccount($account);
|
||||
$model->login = 'erickskrauch';
|
||||
$model->password = '12345678';
|
||||
|
||||
$this->assertNotNull($model->login(), 'model should login user');
|
||||
}
|
||||
|
||||
public function testLoginWithRehashing(): void {
|
||||
/** @var Account $account */
|
||||
$account = $this->tester->grabFixture('accounts', 'user-with-old-password-type');
|
||||
$model = $this->createWithAccount($account);
|
||||
$model->login = $account->username;
|
||||
$model->password = '12345678';
|
||||
|
||||
$this->assertNotNull($model->login());
|
||||
$this->assertSame(Account::PASS_HASH_STRATEGY_YII2, $account->password_hash_strategy);
|
||||
$this->assertNotSame('133c00c463cbd3e491c28cb653ce4718', $account->password_hash);
|
||||
}
|
||||
|
||||
private function createWithAccount(?Account $account): LoginForm {
|
||||
$model = $this->createPartialMock(LoginForm::class, ['getAccount']);
|
||||
$model->method('getAccount')->willReturn($account);
|
||||
|
||||
return $model;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api\tests\unit\models\authentication;
|
||||
|
||||
use api\components\User\Component;
|
||||
use api\models\authentication\LogoutForm;
|
||||
use api\tests\unit\TestCase;
|
||||
use common\models\AccountSession;
|
||||
use Yii;
|
||||
|
||||
class LogoutFormTest extends TestCase {
|
||||
|
||||
public function testNoActionWhenThereIsNoActiveSession(): void {
|
||||
$userComp = $this->createPartialMock(Component::class, ['getActiveSession']);
|
||||
$userComp->method('getActiveSession')->willReturn(null);
|
||||
|
||||
Yii::$app->set('user', $userComp);
|
||||
|
||||
$model = new LogoutForm();
|
||||
$this->assertTrue($model->logout());
|
||||
}
|
||||
|
||||
public function testActiveSessionShouldBeDeleted(): void {
|
||||
$session = $this->createPartialMock(AccountSession::class, ['delete']);
|
||||
$session->expects($this->once())->method('delete')->willReturn(true);
|
||||
|
||||
$userComp = $this->createPartialMock(Component::class, ['getActiveSession']);
|
||||
$userComp->method('getActiveSession')->willReturn($session);
|
||||
|
||||
Yii::$app->set('user', $userComp);
|
||||
|
||||
$model = new LogoutForm();
|
||||
$model->logout();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -5,10 +5,9 @@ namespace api\tests\unit\modules\accounts\models;
|
||||
|
||||
use api\modules\accounts\models\DisableTwoFactorAuthForm;
|
||||
use api\tests\unit\TestCase;
|
||||
use common\helpers\Error as E;
|
||||
use common\models\Account;
|
||||
|
||||
class DisableTwoFactorAuthFormTest extends TestCase {
|
||||
final class DisableTwoFactorAuthFormTest extends TestCase {
|
||||
|
||||
public function testPerformAction(): void {
|
||||
$account = $this->createPartialMock(Account::class, ['save']);
|
||||
@@ -26,18 +25,4 @@ class DisableTwoFactorAuthFormTest extends TestCase {
|
||||
$this->assertFalse($account->is_otp_enabled);
|
||||
}
|
||||
|
||||
public function testValidateOtpEnabled(): void {
|
||||
$account = new Account();
|
||||
$account->is_otp_enabled = false;
|
||||
$model = new DisableTwoFactorAuthForm($account);
|
||||
$model->validateOtpEnabled('account');
|
||||
$this->assertSame([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'));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -6,11 +6,10 @@ namespace api\tests\unit\modules\accounts\models;
|
||||
use api\components\User\Component;
|
||||
use api\modules\accounts\models\EnableTwoFactorAuthForm;
|
||||
use api\tests\unit\TestCase;
|
||||
use common\helpers\Error as E;
|
||||
use common\models\Account;
|
||||
use Yii;
|
||||
|
||||
class EnableTwoFactorAuthFormTest extends TestCase {
|
||||
final class EnableTwoFactorAuthFormTest extends TestCase {
|
||||
|
||||
public function testPerformAction(): void {
|
||||
$account = $this->createPartialMock(Account::class, ['save']);
|
||||
@@ -30,18 +29,4 @@ class EnableTwoFactorAuthFormTest extends TestCase {
|
||||
$this->assertTrue($account->is_otp_enabled);
|
||||
}
|
||||
|
||||
public function testValidateOtpDisabled(): void {
|
||||
$account = new Account();
|
||||
$account->is_otp_enabled = true;
|
||||
$model = new EnableTwoFactorAuthForm($account);
|
||||
$model->validateOtpDisabled('account');
|
||||
$this->assertSame([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'));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,116 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api\tests\unit\modules\authserver\models;
|
||||
|
||||
use api\modules\authserver\exceptions\ForbiddenOperationException;
|
||||
use api\modules\authserver\models\AuthenticationForm;
|
||||
use api\tests\unit\TestCase;
|
||||
use common\models\Account;
|
||||
use common\models\OauthClient;
|
||||
use common\models\OauthSession;
|
||||
use common\tests\fixtures\AccountFixture;
|
||||
use common\tests\fixtures\OauthClientFixture;
|
||||
use OTPHP\TOTP;
|
||||
use function Ramsey\Uuid\v4 as uuid4;
|
||||
|
||||
class AuthenticationFormTest extends TestCase {
|
||||
|
||||
public function _fixtures(): array {
|
||||
return [
|
||||
'accounts' => AccountFixture::class,
|
||||
'oauthClients' => OauthClientFixture::class,
|
||||
];
|
||||
}
|
||||
|
||||
public function testAuthenticateByValidCredentials(): void {
|
||||
$authForm = new AuthenticationForm();
|
||||
$authForm->username = 'admin';
|
||||
$authForm->password = 'password_0';
|
||||
$authForm->clientToken = uuid4();
|
||||
$result = $authForm->authenticate()->getResponseData();
|
||||
$this->assertMatchesRegularExpression('/^[\w=-]+\.[\w=-]+\.[\w=-]+$/', $result['accessToken']);
|
||||
$this->assertSame($authForm->clientToken, $result['clientToken']);
|
||||
$this->assertSame('df936908b2e1544d96f82977ec213022', $result['selectedProfile']['id']);
|
||||
$this->assertSame('Admin', $result['selectedProfile']['name']);
|
||||
$this->assertTrue(OauthSession::find()->andWhere([
|
||||
'account_id' => 1,
|
||||
'client_id' => OauthClient::UNAUTHORIZED_MINECRAFT_GAME_LAUNCHER,
|
||||
])->exists());
|
||||
$this->assertArrayNotHasKey('user', $result);
|
||||
|
||||
$authForm->requestUser = true;
|
||||
$result = $authForm->authenticate()->getResponseData();
|
||||
$this->assertSame([
|
||||
'id' => 'df936908b2e1544d96f82977ec213022',
|
||||
'username' => 'Admin',
|
||||
'properties' => [
|
||||
[
|
||||
'name' => 'preferredLanguage',
|
||||
'value' => 'en',
|
||||
],
|
||||
],
|
||||
], $result['user']);
|
||||
}
|
||||
|
||||
public function testAuthenticateByValidCredentialsWith2FA(): void {
|
||||
$authForm = new AuthenticationForm();
|
||||
$authForm->username = 'otp@gmail.com';
|
||||
$authForm->password = 'password_0:' . TOTP::create('BBBB')->now();
|
||||
$authForm->clientToken = uuid4();
|
||||
|
||||
// Just ensure that there is no exception
|
||||
$this->expectNotToPerformAssertions();
|
||||
|
||||
$authForm->authenticate();
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a special case which ensures that if the user has a password that looks like
|
||||
* a two-factor code passed in the password field, than he can still log in into his account
|
||||
*/
|
||||
public function testAuthenticateEdgyCaseFor2FA(): void {
|
||||
/** @var Account $account */
|
||||
$account = Account::findOne(['email' => 'admin@ely.by']);
|
||||
$account->setPassword('password_0:123456');
|
||||
$account->save();
|
||||
|
||||
$authForm = new AuthenticationForm();
|
||||
$authForm->username = 'admin@ely.by';
|
||||
$authForm->password = 'password_0:123456';
|
||||
$authForm->clientToken = uuid4();
|
||||
|
||||
// Just ensure that there is no exception
|
||||
$this->expectNotToPerformAssertions();
|
||||
|
||||
$authForm->authenticate();
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider getInvalidCredentialsCases
|
||||
*/
|
||||
public function testAuthenticateByWrongCredentials(
|
||||
string $expectedExceptionMessage,
|
||||
string $login,
|
||||
string $password,
|
||||
string $totp = null,
|
||||
): void {
|
||||
$this->expectException(ForbiddenOperationException::class);
|
||||
$this->expectExceptionMessage($expectedExceptionMessage);
|
||||
|
||||
$authForm = new AuthenticationForm();
|
||||
$authForm->username = $login;
|
||||
$authForm->password = $password . ($totp ? ":{$totp}" : '');
|
||||
$authForm->clientToken = uuid4();
|
||||
$authForm->authenticate();
|
||||
}
|
||||
|
||||
public function getInvalidCredentialsCases(): iterable {
|
||||
yield ['Invalid credentials. Invalid nickname or password.', 'wrong-username', 'wrong-password'];
|
||||
yield ['Invalid credentials. Invalid email or password.', 'wrong-email@ely.by', 'wrong-password'];
|
||||
yield ['This account has been suspended.', 'Banned', 'password_0'];
|
||||
yield ['Account protected with two factor auth.', 'AccountWithEnabledOtp', 'password_0'];
|
||||
yield ['Invalid credentials. Invalid nickname or password.', 'AccountWithEnabledOtp', 'password_0', '123456'];
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,15 +1,17 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api\tests\unit\validators;
|
||||
|
||||
use api\tests\unit\TestCase;
|
||||
use api\validators\TotpValidator;
|
||||
use Carbon\CarbonImmutable;
|
||||
use common\helpers\Error as E;
|
||||
use common\models\Account;
|
||||
use common\tests\_support\ProtectedCaller;
|
||||
use Lcobucci\Clock\FrozenClock;
|
||||
use OTPHP\TOTP;
|
||||
|
||||
class TotpValidatorTest extends TestCase {
|
||||
use ProtectedCaller;
|
||||
final class TotpValidatorTest extends TestCase {
|
||||
|
||||
public function testValidateValue(): void {
|
||||
$account = new Account();
|
||||
@@ -18,32 +20,25 @@ class TotpValidatorTest extends TestCase {
|
||||
|
||||
$validator = new TotpValidator(['account' => $account]);
|
||||
|
||||
$result = $this->callProtected($validator, 'validateValue', 123456);
|
||||
$this->assertSame([E::TOTP_INCORRECT, []], $result);
|
||||
$this->assertFalse($validator->validate(123456, $error));
|
||||
$this->assertSame(E::TOTP_INCORRECT, $error);
|
||||
|
||||
$result = $this->callProtected($validator, 'validateValue', $controlTotp->now());
|
||||
$this->assertNull($result);
|
||||
$error = null;
|
||||
|
||||
$result = $this->callProtected($validator, 'validateValue', $controlTotp->at(time() - 31));
|
||||
$this->assertNull($result);
|
||||
$this->assertTrue($validator->validate($controlTotp->now(), $error));
|
||||
$this->assertNull($error);
|
||||
|
||||
$at = time() - 400;
|
||||
$validator->timestamp = $at;
|
||||
$result = $this->callProtected($validator, 'validateValue', $controlTotp->now());
|
||||
$this->assertSame([E::TOTP_INCORRECT, []], $result);
|
||||
$error = null;
|
||||
|
||||
$result = $this->callProtected($validator, 'validateValue', $controlTotp->at($at));
|
||||
$this->assertNull($result);
|
||||
// @phpstan-ignore argument.type
|
||||
$this->assertTrue($validator->validate($controlTotp->at(time() - 31), $error));
|
||||
$this->assertNull($error);
|
||||
|
||||
$at = fn(): ?int => null;
|
||||
$validator->timestamp = $at;
|
||||
$result = $this->callProtected($validator, 'validateValue', $controlTotp->now());
|
||||
$this->assertNull($result);
|
||||
$error = null;
|
||||
|
||||
$at = fn(): int => time() - 700;
|
||||
$validator->timestamp = $at;
|
||||
$result = $this->callProtected($validator, 'validateValue', $controlTotp->at($at()));
|
||||
$this->assertNull($result);
|
||||
$validator->setClock(new FrozenClock(CarbonImmutable::now()->subSeconds(400)));
|
||||
$this->assertFalse($validator->validate($controlTotp->now(), $error));
|
||||
$this->assertSame(E::TOTP_INCORRECT, $error);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user