Extract login logics into a separate component. Not quite clean result but enough for upcoming tasks

This commit is contained in:
ErickSkrauch
2025-01-17 21:37:35 +01:00
parent 1c2969a4be
commit be4697e6eb
39 changed files with 443 additions and 729 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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