anonymous//tests/Model/Traits/HasOtpTest.php$0   A
last analyzed

Complexity

Total Complexity 1

Size/Duplication

Total Lines 6
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 1
c 1
b 0
f 0
dl 0
loc 6
rs 10
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
It seems like $otp1 can also be of type null; however, parameter $string of PHPUnit\Framework\Assert::assertStringStartsWith() 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 ignore-type  annotation

36
        self::assertStringStartsWith('otpauth://totp/', /** @scrutinizer ignore-type */ $otp1, 'TOTP provisioning URI was generated and stored');
Loading history...
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
Bug introduced by
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 ignore-type  annotation

91
        $otp = Factory::loadFromProvisioningUri(/** @scrutinizer ignore-type */ $uri);
Loading history...
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