Ecodev /
felix
| 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
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
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
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 |