Skip to content

Commit f63822d

Browse files
committed
OXDEV-9078 Add resend cooldown checking service
1 parent ec53b45 commit f63822d

3 files changed

Lines changed: 155 additions & 0 deletions

File tree

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
3+
/**
4+
* Copyright © OXID eSales AG. All rights reserved.
5+
* See LICENSE file for license details.
6+
*/
7+
8+
declare(strict_types=1);
9+
10+
namespace OxidEsales\SecurityModule\Authentication\TwoFactorAuth\OTP\Service;
11+
12+
use DateTimeImmutable;
13+
use OxidEsales\SecurityModule\Authentication\TwoFactorAuth\OTP\Infrastructure\Repository\OtpChallengeStateRepositoryInterface;
14+
15+
class OtpSendPolicyService implements OtpSendPolicyServiceInterface
16+
{
17+
private const RESEND_COOLDOWN_SECONDS = 60;
18+
19+
public function __construct(
20+
private OtpChallengeStateRepositoryInterface $challengeStateRepository,
21+
) {
22+
}
23+
24+
public function canSend(string $userId): bool
25+
{
26+
$state = $this->challengeStateRepository->findByUserId($userId);
27+
28+
if ($state === null) {
29+
return true;
30+
}
31+
32+
$lastSentAt = $state->getLastSentAt();
33+
if ($lastSentAt === null) {
34+
return true;
35+
}
36+
37+
$elapsed = (new DateTimeImmutable())->getTimestamp() - $lastSentAt->getTimestamp();
38+
39+
return $elapsed >= self::RESEND_COOLDOWN_SECONDS;
40+
}
41+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
/**
4+
* Copyright © OXID eSales AG. All rights reserved.
5+
* See LICENSE file for license details.
6+
*/
7+
8+
namespace OxidEsales\SecurityModule\Authentication\TwoFactorAuth\OTP\Service;
9+
10+
interface OtpSendPolicyServiceInterface
11+
{
12+
public function canSend(string $userId): bool;
13+
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
<?php
2+
3+
/**
4+
* Copyright © OXID eSales AG. All rights reserved.
5+
* See LICENSE file for license details.
6+
*/
7+
8+
declare(strict_types=1);
9+
10+
namespace OxidEsales\SecurityModule\Tests\Unit\Authentication\TwoFactorAuth\OTP\Service;
11+
12+
use DateTimeImmutable;
13+
use OxidEsales\SecurityModule\Authentication\TwoFactorAuth\OTP\DTO\OtpChallengeStateInterface;
14+
use OxidEsales\SecurityModule\Authentication\TwoFactorAuth\OTP\Infrastructure\Repository\OtpChallengeStateRepositoryInterface;
15+
use OxidEsales\SecurityModule\Authentication\TwoFactorAuth\OTP\Service\OtpSendPolicyService;
16+
use OxidEsales\SecurityModule\Authentication\TwoFactorAuth\OTP\Service\OtpSendPolicyServiceInterface;
17+
use PHPUnit\Framework\Attributes\Test;
18+
use PHPUnit\Framework\TestCase;
19+
20+
class OtpSendPolicyServiceTest extends TestCase
21+
{
22+
#[Test]
23+
public function canSendReturnsTrueWhenNoStateExists(): void
24+
{
25+
$userId = uniqid();
26+
27+
$repositoryMock = $this->createMock(OtpChallengeStateRepositoryInterface::class);
28+
$repositoryMock->method('findByUserId')
29+
->with($userId)
30+
->willReturn(null);
31+
32+
$sut = $this->getSut(challengeStateRepository: $repositoryMock);
33+
34+
$this->assertTrue($sut->canSend($userId));
35+
}
36+
37+
#[Test]
38+
public function canSendReturnsTrueWhenLastSentAtIsNull(): void
39+
{
40+
$userId = uniqid();
41+
42+
$stateStub = $this->createStub(OtpChallengeStateInterface::class);
43+
$stateStub->method('getLastSentAt')
44+
->willReturn(null);
45+
46+
$repositoryMock = $this->createMock(OtpChallengeStateRepositoryInterface::class);
47+
$repositoryMock->method('findByUserId')
48+
->with($userId)
49+
->willReturn($stateStub);
50+
51+
$sut = $this->getSut(challengeStateRepository: $repositoryMock);
52+
53+
$this->assertTrue($sut->canSend($userId));
54+
}
55+
56+
#[Test]
57+
public function canSendReturnsTrueWhenCooldownHasPassed(): void
58+
{
59+
$userId = uniqid();
60+
61+
$stateStub = $this->createStub(OtpChallengeStateInterface::class);
62+
$stateStub->method('getLastSentAt')
63+
->willReturn(new DateTimeImmutable('-61 seconds'));
64+
65+
$repositoryMock = $this->createMock(OtpChallengeStateRepositoryInterface::class);
66+
$repositoryMock->method('findByUserId')
67+
->with($userId)
68+
->willReturn($stateStub);
69+
70+
$sut = $this->getSut(challengeStateRepository: $repositoryMock);
71+
72+
$this->assertTrue($sut->canSend($userId));
73+
}
74+
75+
#[Test]
76+
public function canSendReturnsFalseWhenCooldownHasNotPassed(): void
77+
{
78+
$userId = uniqid();
79+
80+
$stateStub = $this->createStub(OtpChallengeStateInterface::class);
81+
$stateStub->method('getLastSentAt')
82+
->willReturn(new DateTimeImmutable('-30 seconds'));
83+
84+
$repositoryMock = $this->createMock(OtpChallengeStateRepositoryInterface::class);
85+
$repositoryMock->method('findByUserId')
86+
->with($userId)
87+
->willReturn($stateStub);
88+
89+
$sut = $this->getSut(challengeStateRepository: $repositoryMock);
90+
91+
$this->assertFalse($sut->canSend($userId));
92+
}
93+
94+
private function getSut(
95+
OtpChallengeStateRepositoryInterface $challengeStateRepository = null,
96+
): OtpSendPolicyServiceInterface {
97+
return new OtpSendPolicyService(
98+
challengeStateRepository: $challengeStateRepository ?? $this->createStub(OtpChallengeStateRepositoryInterface::class),
99+
);
100+
}
101+
}

0 commit comments

Comments
 (0)