1 | <?php |
||||
2 | |||||
3 | declare(strict_types=1); |
||||
4 | |||||
5 | namespace EcodevTests\Felix\Model\Traits; |
||||
6 | |||||
7 | use Ecodev\Felix\Model\Traits\HasOtp; |
||||
8 | use OTPHP\Factory; |
||||
9 | use OTPHP\TOTPInterface; |
||||
10 | use PHPUnit\Framework\TestCase; |
||||
11 | |||||
12 | final class HasOtpTest extends TestCase |
||||
13 | { |
||||
14 | private \Ecodev\Felix\Model\HasOtp $user; |
||||
15 | |||||
16 | protected function setUp(): void |
||||
17 | { |
||||
18 | $this->user = new class() implements \Ecodev\Felix\Model\HasOtp { |
||||
19 | use HasOtp; |
||||
20 | |||||
21 | public function getLogin(): ?string |
||||
22 | { |
||||
23 | return '[email protected]'; |
||||
24 | } |
||||
25 | }; |
||||
26 | } |
||||
27 | |||||
28 | public function testCreateOtpSecret(): void |
||||
29 | { |
||||
30 | self::assertNull($this->user->getOtpUri(), 'should have no OTP secret at first'); |
||||
31 | self::assertFalse($this->user->isOtp(), 'should have OTP disabled at first'); |
||||
32 | |||||
33 | $this->user->createOtpSecret('felix.lan'); |
||||
34 | $otp1 = $this->user->getOtpUri(); |
||||
35 | self::assertIsString($otp1); |
||||
36 | self::assertStringStartsWith('otpauth://totp/', $otp1, 'TOTP provisioning URI was generated and stored'); |
||||
0 ignored issues
–
show
Bug
introduced
by
![]() |
|||||
37 | |||||
38 | $this->user->createOtpSecret('felix.lan'); |
||||
39 | $otp2 = $this->user->getOtpUri(); |
||||
40 | self::assertIsString($otp2); |
||||
41 | self::assertNotSame($otp1, $otp2, 'TOTP provisioning URI was changed'); |
||||
42 | |||||
43 | $this->user->setOtp(true); |
||||
44 | self::assertTrue($this->user->isOtp()); |
||||
45 | } |
||||
46 | |||||
47 | public function testCreateOtpSecretWillThrowWithoutSecret(): void |
||||
48 | { |
||||
49 | $this->expectExceptionMessage('Cannot enable OTP without a secret'); |
||||
50 | $this->user->setOtp(true); |
||||
51 | } |
||||
52 | |||||
53 | public function testCreateOtpSecretWillThrowWithoutLogin(): void |
||||
54 | { |
||||
55 | $user = new class() implements \Ecodev\Felix\Model\HasOtp { |
||||
56 | use HasOtp; |
||||
57 | |||||
58 | public function getLogin(): ?string |
||||
59 | { |
||||
60 | return null; |
||||
61 | } |
||||
62 | }; |
||||
63 | |||||
64 | $this->expectExceptionMessage('User must have a login to initialize OTP'); |
||||
65 | $user->createOtpSecret('felix.lan'); |
||||
66 | } |
||||
67 | |||||
68 | public function testRevokeSecret(): void |
||||
69 | { |
||||
70 | $this->user->createOtpSecret('felix.lan'); |
||||
71 | $this->user->revokeOtpSecret(); |
||||
72 | |||||
73 | self::assertFalse($this->user->isOtp()); |
||||
74 | self::assertNull($this->user->getOtpUri()); |
||||
75 | } |
||||
76 | |||||
77 | public function testVerifySecret(): void |
||||
78 | { |
||||
79 | $this->user->setOtp(false); |
||||
80 | self::assertFalse($this->user->verifyOtp('123456'), 'Cannot verify OTP with 2FA disabled'); |
||||
81 | |||||
82 | $this->user->createOtpSecret('felix.lan'); |
||||
83 | $this->user->setOtp(true); |
||||
84 | |||||
85 | self::assertFalse($this->user->verifyOtp('123456'), 'Wrong OTP given'); |
||||
86 | self::assertFalse($this->user->verifyOtp(''), 'Empty OTP given'); |
||||
87 | |||||
88 | $uri = $this->user->getOtpUri(); |
||||
89 | self::assertNotEmpty($uri); |
||||
90 | |||||
91 | $otp = Factory::loadFromProvisioningUri($uri); |
||||
0 ignored issues
–
show
It seems like
$uri can also be of type null ; however, parameter $uri of OTPHP\Factory::loadFromProvisioningUri() does only seem to accept string , maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
92 | self::assertInstanceOf(TOTPInterface::class, $otp); |
||||
93 | |||||
94 | // This is very time sensitive, and test might be flaky if the generated OTP is on the last |
||||
95 | // millisecond of a second, and the verification happens on the first millisecond of the next second. |
||||
96 | // To limit flakiness, we test with a slightly shorter time period than what is actually allowed. |
||||
97 | self::assertTrue($this->user->verifyOtp($otp->at(time())), 'Correct OTP given'); |
||||
98 | self::assertTrue($this->user->verifyOtp($otp->at(time() - 27)), 'Even accept correct past OTP, in case of mobile device clock sync failure'); |
||||
99 | self::assertTrue($this->user->verifyOtp($otp->at(time() + 27)), 'Even accept correct future OTP, in case of mobile device clock sync failure'); |
||||
100 | } |
||||
101 | } |
||||
102 |