Skip to content

Commit 801bba0

Browse files
committed
OXDEV-9078 Add codeception tests for two FA
1 parent 63d75ad commit 801bba0

6 files changed

Lines changed: 193 additions & 20 deletions

File tree

src/Authentication/TwoFactorAuth/Controller/AccountSecurityController.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,9 @@ public function render(): string
2727
$parentResult = parent::render();
2828

2929
$user = $this->getUser();
30-
31-
$this->addTplParam('twoFAEnabledForUser', $this->userSettingsService->isEnabledForUser($user->getId()));
30+
if ($user) {
31+
$this->addTplParam('twoFAEnabledForUser', $this->userSettingsService->isEnabledForUser($user->getId()));
32+
}
3233

3334
return $parentResult;
3435
}

src/Captcha/Service/ModuleSettingsService.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,11 @@ public function isHoneyPotCaptchaEnabled(): bool
4747
return $this->moduleSettingService->getBoolean(self::HONEYPOT_CAPTCHA_ENABLE, Module::MODULE_ID);
4848
}
4949

50+
public function saveIsHoneyPotCaptchaEnabled(bool $value): void
51+
{
52+
$this->moduleSettingService->saveBoolean(self::HONEYPOT_CAPTCHA_ENABLE, $value, Module::MODULE_ID);
53+
}
54+
5055
public function getCaptchaLifeTime(): string
5156
{
5257
$key = $this->moduleSettingService

src/Captcha/Service/ModuleSettingsServiceInterface.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,7 @@ public function isHoneyPotCaptchaEnabled(): bool;
1515

1616
public function saveIsCaptchaEnabled(bool $value): void;
1717

18+
public function saveIsHoneyPotCaptchaEnabled(bool $value): void;
19+
1820
public function getCaptchaLifeTime(): string;
1921
}

tests/Codeception/Acceptance/BaseCest.php

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,10 @@
1414
use OxidEsales\EshopCommunity\Internal\Framework\Module\Facade\ModuleSettingServiceInterface;
1515
use OxidEsales\SecurityModule\Authentication\OAuth2\Service\ModuleSettingsServiceInterface
1616
as OAuthModuleSettingsServiceInterface;
17-
use OxidEsales\SecurityModule\Authentication\TwoFactorAuth\Settings\TwoFASettings;
17+
use OxidEsales\SecurityModule\Authentication\TwoFactorAuth\Settings\TwoFAShopSettings;
1818
use OxidEsales\SecurityModule\Captcha\Service\ModuleSettingsServiceInterface as CaptchaSettingsServiceInterface;
1919
use OxidEsales\SecurityModule\Core\Module;
20+
use OxidEsales\SecurityModule\Tests\Codeception\Support\AcceptanceTester;
2021
use OxidEsales\SecurityModule\PasswordPolicy\Service\ModuleSettingsServiceInterface as PasswordSettingsServiceInterface;
2122

2223
abstract class BaseCest
@@ -41,15 +42,30 @@ protected function setCaptchaState(bool $state)
4142
ContainerFacade::get(CaptchaSettingsServiceInterface::class)->saveIsCaptchaEnabled($state);
4243
}
4344

45+
protected function setHoneyPotCaptchaState(bool $state)
46+
{
47+
ContainerFacade::get(CaptchaSettingsServiceInterface::class)->saveIsHoneyPotCaptchaEnabled($state);
48+
}
49+
4450
protected function setTwoFactorAuthState(bool $state)
4551
{
4652
ContainerFacade::get(ModuleSettingServiceInterface::class)->saveBoolean(
47-
TwoFASettings::ACTIVE,
53+
TwoFAShopSettings::ACTIVE,
4854
$state,
4955
Module::MODULE_ID
5056
);
5157
}
5258

59+
protected function setUserTwoFAState(AcceptanceTester $I, bool $state): void
60+
{
61+
$userData = $this->getExistingUserData();
62+
$I->updateInDatabase(
63+
'oxuser',
64+
['OE2FAENABLED' => (int) $state],
65+
['OXID' => $userData['userId']]
66+
);
67+
}
68+
5369
protected function setProviderState(bool $state)
5470
{
5571
$moduleSettings = ContainerFacade::get(OAuthModuleSettingsServiceInterface::class);

tests/Codeception/Acceptance/HoneyPotCaptchaCest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ class HoneyPotCaptchaCest extends BaseCest
3333
public function _before(AcceptanceTester $I): void
3434
{
3535
$this->setCaptchaState(false);
36+
$this->setHoneyPotCaptchaState(true);
3637
$this->setPasswordState(false);
3738
}
3839

tests/Codeception/Acceptance/TwoFAAuthenticationCest.php

Lines changed: 164 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -23,47 +23,195 @@
2323
class TwoFAAuthenticationCest extends BaseCest
2424
{
2525
private string $otpInput = '#auth_code';
26-
private string $otpSubmitBtn = '#auth_submit';
27-
private string $otpResendBtn = 'RESEND_CODE';
26+
private string $twofaCheckbox = '#twofa_enabled';
2827

2928
public function _before(AcceptanceTester $I): void
3029
{
3130
$this->setCaptchaState(false);
3231
$this->setPasswordState(false);
3332
$this->setTwoFactorAuthState(true);
33+
$this->setUserTwoFAState($I, false);
3434
}
3535

36-
public function testRedirectToOTPAfterLogin(AcceptanceTester $I): void
36+
public function testEnablingTwoFAViaSettingsTriggersOtpOnNextLogin(AcceptanceTester $I): void
3737
{
3838
$userData = $this->getExistingUserData();
39+
$userLoginPage = new UserLogin($I);
40+
41+
$I->amOnPage($userLoginPage->URL);
42+
$userAccountPage = $userLoginPage->login($userData['userLoginName'], $userData['userPassword']);
43+
$I->waitForPageLoad();
44+
45+
$I->amOnPage('?cl=account_security');
46+
$I->waitForPageLoad();
47+
$I->checkOption($this->twofaCheckbox);
48+
$I->click(Translator::translate('SAVE'));
49+
$I->waitForPageLoad();
50+
51+
$userAccountPage->logoutUserInAccountPage();
52+
$I->waitForPageLoad();
53+
54+
$I->amOnPage($userLoginPage->URL);
55+
$userLoginPage->login($userData['userLoginName'], $userData['userPassword']);
56+
$I->waitForPageLoad();
3957

58+
$I->seeElement($this->otpInput);
59+
}
60+
61+
public function testLoginOtpBehaviourChangesWithUserTwoFASetting(AcceptanceTester $I): void
62+
{
63+
$userData = $this->getExistingUserData();
4064
$userLoginPage = new UserLogin($I);
65+
66+
$this->setUserTwoFAState($I, true);
67+
68+
$I->amOnPage($userLoginPage->URL);
69+
$userLoginPage->login($userData['userLoginName'], $userData['userPassword']);
70+
$I->waitForPageLoad();
71+
$I->seeElement($this->otpInput);
72+
73+
$this->setUserTwoFAState($I, false);
74+
4175
$I->amOnPage($userLoginPage->URL);
76+
$userLoginPage->login($userData['userLoginName'], $userData['userPassword']);
77+
$I->waitForPageLoad();
78+
$I->dontSeeElement($this->otpInput);
79+
}
80+
81+
public function testSettingsPageReflectsTwoFAState(AcceptanceTester $I): void
82+
{
83+
$userData = $this->getExistingUserData();
84+
$userLoginPage = new UserLogin($I);
85+
86+
$I->amOnPage($userLoginPage->URL);
87+
$userLoginPage->login($userData['userLoginName'], $userData['userPassword']);
88+
$I->waitForPageLoad();
89+
90+
$I->amOnPage('?cl=account_security');
91+
$I->waitForPageLoad();
92+
$I->dontSeeCheckboxIsChecked($this->twofaCheckbox);
93+
94+
$I->checkOption($this->twofaCheckbox);
95+
$I->click(Translator::translate('SAVE'));
96+
$I->waitForPageLoad();
97+
98+
$I->amOnPage('?cl=account_security');
99+
$I->waitForPageLoad();
100+
$I->seeCheckboxIsChecked($this->twofaCheckbox);
101+
102+
$I->uncheckOption($this->twofaCheckbox);
103+
$I->click(Translator::translate('SAVE'));
104+
$I->waitForPageLoad();
105+
106+
$I->amOnPage('?cl=account_security');
107+
$I->waitForPageLoad();
108+
$I->dontSeeCheckboxIsChecked($this->twofaCheckbox);
109+
}
110+
111+
public function testUnauthenticatedAccessToAccountSecurityRedirectsToLogin(AcceptanceTester $I): void
112+
{
113+
$I->amOnPage('?cl=account_security');
114+
$I->waitForPageLoad();
115+
42116
$I->see(Translator::translate('LOGIN'));
117+
$I->dontSeeElement($this->twofaCheckbox);
118+
}
119+
120+
public function testShopLevelTwoFADisabledOverridesUserSetting(AcceptanceTester $I): void
121+
{
122+
$this->setUserTwoFAState($I, true);
123+
$this->setTwoFactorAuthState(false);
124+
125+
$userData = $this->getExistingUserData();
126+
$userLoginPage = new UserLogin($I);
127+
$I->amOnPage($userLoginPage->URL);
128+
$userLoginPage->login($userData['userLoginName'], $userData['userPassword']);
129+
$I->waitForPageLoad();
130+
131+
$I->dontSeeElement($this->otpInput);
132+
}
133+
134+
public function testInvalidCodeShowsError(AcceptanceTester $I): void
135+
{
136+
$this->setUserTwoFAState($I, true);
43137

138+
$userData = $this->getExistingUserData();
139+
$userLoginPage = new UserLogin($I);
140+
$I->amOnPage($userLoginPage->URL);
44141
$userLoginPage->login($userData['userLoginName'], $userData['userPassword']);
142+
$I->waitForPageLoad();
45143

144+
$I->fillField($this->otpInput, '000000');
145+
$I->click('#auth_submit');
46146
$I->waitForPageLoad();
47-
$I->seeElement($this->otpInput);
48-
$I->seeElement($this->otpSubmitBtn);
49-
$I->see(Translator::translate($this->otpResendBtn));
147+
148+
$I->see(Translator::translate('ERROR_INVALID_CODE'));
50149
}
51150

52-
public function testRedirectToOTPOnLoginBox(AcceptanceTester $I): void
151+
public function testWrongCodeDecreasesRemainingAttemptsAndShowsError(AcceptanceTester $I): void
53152
{
153+
$this->setUserTwoFAState($I, true);
54154

55155
$userData = $this->getExistingUserData();
156+
$userLoginPage = new UserLogin($I);
157+
$I->amOnPage($userLoginPage->URL);
158+
$userLoginPage->login($userData['userLoginName'], $userData['userPassword']);
159+
$I->waitForPageLoad();
56160

57-
$homePage = $I->openShop();
58-
$accountMenu = $homePage->openAccountMenu();
59-
$I->waitForText(Translator::translate('FORGOT_PASSWORD'));
60-
$I->retryFillField($accountMenu->userLoginName, $userData['userLoginName']);
61-
$I->retryFillField($accountMenu->userLoginPassword, $userData['userPassword']);
62-
$I->retryClick($accountMenu->userLoginButton);
161+
$attemptsBefore = (int) $I->grabTextFrom('#remaining-attempts');
63162

163+
$I->fillField($this->otpInput, '000000');
164+
$I->click('#auth_submit');
64165
$I->waitForPageLoad();
65-
$I->seeElement($this->otpInput);
66-
$I->seeElement($this->otpSubmitBtn);
67-
$I->see(Translator::translate($this->otpResendBtn));
166+
167+
$I->see(Translator::translate('ERROR_INVALID_CODE'));
168+
$I->see((string) ($attemptsBefore - 1), '#remaining-attempts');
169+
}
170+
171+
public function testResendButtonIsDisabledOnOtpPageLoad(AcceptanceTester $I): void
172+
{
173+
$this->setUserTwoFAState($I, true);
174+
175+
$userData = $this->getExistingUserData();
176+
$userLoginPage = new UserLogin($I);
177+
$I->amOnPage($userLoginPage->URL);
178+
$userLoginPage->login($userData['userLoginName'], $userData['userPassword']);
179+
$I->waitForPageLoad();
180+
181+
// A code was just sent at login, so the server-driven cooldown should disable the button
182+
$I->waitForJS("return document.getElementById('resend-btn').disabled === true", 5);
183+
$I->seeElement('#resend-btn[disabled]');
184+
}
185+
186+
public function testResendButtonShowsCountdownAfterClick(AcceptanceTester $I): void
187+
{
188+
$this->setUserTwoFAState($I, true);
189+
190+
$userData = $this->getExistingUserData();
191+
$userLoginPage = new UserLogin($I);
192+
$I->amOnPage($userLoginPage->URL);
193+
$userLoginPage->login($userData['userLoginName'], $userData['userPassword']);
194+
$I->waitForPageLoad();
195+
196+
// Backdate LAST_SENT_AT so the server reports zero remaining cooldown
197+
$I->updateInDatabase(
198+
'oesm_2fa_otp',
199+
['LAST_SENT_AT' => date('Y-m-d H:i:s', strtotime('-2 minutes'))],
200+
['OXUSERID' => $userData['userId']]
201+
);
202+
203+
// Clear stored cooldown from localStorage so JS reads from the (now zero) server value
204+
$I->executeJS("localStorage.clear()");
205+
$I->reloadPage();
206+
$I->waitForPageLoad();
207+
208+
$I->seeElement('#resend-btn:not([disabled])');
209+
210+
$I->click('#resend-btn');
211+
212+
$I->waitForJS(
213+
"return document.getElementById('resend-btn').textContent.includes('Resend in')",
214+
30
215+
);
68216
}
69217
}

0 commit comments

Comments
 (0)