diff --git a/.github/workflows/infection.yml b/.github/workflows/infection.yml new file mode 100644 index 0000000..526db27 --- /dev/null +++ b/.github/workflows/infection.yml @@ -0,0 +1,34 @@ +# yaml-language-server: $schema=https://json.schemastore.org/github-workflow + +name: "Infection" + +on: + push: + branches: + - "*.x" + +jobs: + mutation_testing: + name: "5️⃣ Mutation Testing" + steps: + - name: "Set up PHP" + uses: "shivammathur/setup-php@v2" + with: + php-version: "8.1" + extensions: "mbstring" + coverage: "xdebug" + + - name: "Checkout code" + uses: "actions/checkout@v3" + + - name: "Fetch Git base reference" + run: "git fetch --depth=1 origin ${GITHUB_BASE_REF}" + + - name: "Install dependencies" + uses: "ramsey/composer-install@v2" + with: + dependency-versions: "highest" + composer-options: "--optimize-autoloader" + + - name: "Execute Infection" + run: "make ci-mu" diff --git a/.github/workflows/integrate.yml b/.github/workflows/integrate.yml index 5578d06..4cd88ab 100644 --- a/.github/workflows/integrate.yml +++ b/.github/workflows/integrate.yml @@ -56,6 +56,8 @@ jobs: - "ubuntu-latest" php-version: - "8.1" + - "8.2" + - "8.3" dependencies: - "lowest" - "highest" @@ -80,14 +82,6 @@ jobs: - name: "Execute tests (PHP)" run: "make ci-cc" - # - name: Send coverage to Coveralls - # if: "matrix.php-version == '8.1' && matrix.dependencies == 'highest'" - # env: - # COVERALLS_REPO_TOKEN: "${{ secrets.GITHUB_TOKEN }}" - # run: | - # wget "https://github.com/php-coveralls/php-coveralls/releases/download/v2.5.2/php-coveralls.phar" - # php ./php-coveralls.phar -v - static_analysis: name: "3️⃣ Static Analysis" needs: @@ -153,35 +147,6 @@ jobs: run: | vendor/bin/deptrac analyse --fail-on-uncovered --no-cache - mutation_testing: - name: "5️⃣ Mutation Testing" - needs: - - "byte_level" - - "syntax_errors" - runs-on: "ubuntu-latest" - steps: - - name: "Set up PHP" - uses: "shivammathur/setup-php@v2" - with: - php-version: "8.1" - extensions: "mbstring" - coverage: "xdebug" - - - name: "Checkout code" - uses: "actions/checkout@v3" - - - name: "Fetch Git base reference" - run: "git fetch --depth=1 origin ${GITHUB_BASE_REF}" - - - name: "Install dependencies" - uses: "ramsey/composer-install@v2" - with: - dependency-versions: "highest" - composer-options: "--optimize-autoloader" - - - name: "Execute Infection" - run: "make ci-mu" - rector_checkstyle: name: "6️⃣ Rector Checkstyle" needs: diff --git a/README.md b/README.md index b23282e..542de6f 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ TOTP / HOTP library in PHP ========================== ![Build Status](https://github.com/spomky-labs/otphp/workflows/Integrate/badge.svg) +![Build Status](https://github.com/spomky-labs/otphp/workflows/Infection/badge.svg) [![Latest Stable Version](https://poser.pugx.org/spomky-labs/otphp/v/stable.png)](https://packagist.org/packages/spomky-labs/otphp) [![Total Downloads](https://poser.pugx.org/spomky-labs/otphp/downloads.png)](https://packagist.org/packages/spomky-labs/otphp) diff --git a/composer.json b/composer.json index 080df37..33901f7 100644 --- a/composer.json +++ b/composer.json @@ -22,17 +22,17 @@ }, "require-dev": { "ekino/phpstan-banned-code": "^1.0", - "infection/infection": "^0.26", + "infection/infection": "^0.26|^0.27|^0.28", "php-parallel-lint/php-parallel-lint": "^1.3", "phpstan/phpstan": "^1.0", "phpstan/phpstan-deprecation-rules": "^1.0", "phpstan/phpstan-phpunit": "^1.0", "phpstan/phpstan-strict-rules": "^1.0", - "phpunit/phpunit": "^9.5.26", + "phpunit/phpunit": "^9.5.26|^10.0|^11.0", "qossmic/deptrac-shim": "^1.0", - "rector/rector": "^0.15", - "symfony/phpunit-bridge": "^6.1", - "symplify/easy-coding-standard": "^11.0" + "rector/rector": "1.0", + "symfony/phpunit-bridge": "^6.1|^7.0", + "symplify/easy-coding-standard": "^12.0" }, "autoload": { "psr-4": { "OTPHP\\": "src/" } diff --git a/ecs.php b/ecs.php index fc51758..8df6aa3 100644 --- a/ecs.php +++ b/ecs.php @@ -30,9 +30,7 @@ use Symplify\EasyCodingStandard\Config\ECSConfig; use Symplify\EasyCodingStandard\ValueObject\Set\SetList; -$header = ''; - -return static function (ECSConfig $config) use ($header): void { +return static function (ECSConfig $config): void { $config->import(SetList::PSR_12); $config->import(SetList::CLEAN_CODE); $config->import(SetList::DOCTRINE_ANNOTATIONS); @@ -74,7 +72,7 @@ 'strict' => true, ]); $config->ruleWithConfiguration(HeaderCommentFixer::class, [ - 'header' => $header, + 'header' => '', ]); $config->ruleWithConfiguration(AlignMultilineCommentFixer::class, [ 'comment_type' => 'all_multiline', @@ -88,13 +86,7 @@ 'import_functions' => true, ]); - $config->services() - ->remove(PhpUnitTestClassRequiresCoversFixer::class) - ; - $config->parallel(); - $config->paths([ - __DIR__.'/src', - __DIR__.'/tests', - ]); + $config->paths([__DIR__]); + $config->skip([__DIR__ . '/vendor', PhpUnitTestClassRequiresCoversFixer::class]); }; diff --git a/phpunit.xml.dist b/phpunit.xml.dist index e52c02c..61963d4 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -3,26 +3,16 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" bootstrap="vendor/autoload.php" colors="true" - resolveDependencies="true" - xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd" + xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd" > - - - ./src - - ./tests - - - - - OTPHP\TOTP - - - - + + + ./src + + diff --git a/rector.php b/rector.php index d781ad4..d82cf27 100644 --- a/rector.php +++ b/rector.php @@ -3,36 +3,24 @@ declare(strict_types=1); use Rector\Config\RectorConfig; -use Rector\Core\ValueObject\PhpVersion; -use Rector\Doctrine\Set\DoctrineSetList; -use Rector\Php74\Rector\Property\TypedPropertyRector; -use Rector\PHPUnit\Set\PHPUnitLevelSetList; +use Rector\PHPUnit\CodeQuality\Rector\Class_\PreferPHPUnitThisCallRector; use Rector\PHPUnit\Set\PHPUnitSetList; use Rector\Set\ValueObject\LevelSetList; use Rector\Set\ValueObject\SetList; -use Rector\Symfony\Set\SymfonyLevelSetList; use Rector\Symfony\Set\SymfonySetList; +use Rector\ValueObject\PhpVersion; return static function (RectorConfig $config): void { - $config->sets([ - SetList::DEAD_CODE, - LevelSetList::UP_TO_PHP_81, - SymfonyLevelSetList::UP_TO_SYMFONY_54, - SymfonySetList::SYMFONY_CODE_QUALITY, - SymfonySetList::SYMFONY_CONSTRUCTOR_INJECTION, - DoctrineSetList::DOCTRINE_CODE_QUALITY, - DoctrineSetList::ANNOTATIONS_TO_ATTRIBUTES, - PHPUnitSetList::PHPUNIT_SPECIFIC_METHOD, - PHPUnitLevelSetList::UP_TO_PHPUNIT_100, - PHPUnitSetList::PHPUNIT_CODE_QUALITY, - PHPUnitSetList::PHPUNIT_EXCEPTION, - PHPUnitSetList::REMOVE_MOCKS, - PHPUnitSetList::PHPUNIT_YIELD_DATA_PROVIDER, - ]); + $config->import(SetList::DEAD_CODE); + $config->import(LevelSetList::UP_TO_PHP_80); + $config->import(SymfonySetList::SYMFONY_CODE_QUALITY); + $config->import(PHPUnitSetList::PHPUNIT_100); + $config->import(PHPUnitSetList::ANNOTATIONS_TO_ATTRIBUTES); + $config->import(PHPUnitSetList::PHPUNIT_CODE_QUALITY); $config->parallel(); - $config->paths([__DIR__ . '/src']); + $config->paths([__DIR__ . '/src', __DIR__ . '/tests']); + $config->skip([PreferPHPUnitThisCallRector::class]); $config->phpVersion(PhpVersion::PHP_81); $config->importNames(); $config->importShortClasses(); - }; diff --git a/src/Factory.php b/src/Factory.php index 409d875..f58e838 100644 --- a/src/Factory.php +++ b/src/Factory.php @@ -4,10 +4,10 @@ namespace OTPHP; -use function assert; -use function count; use InvalidArgumentException; use Throwable; +use function assert; +use function count; /** * This class is used to load OTP object from a provisioning Uri. diff --git a/src/OTP.php b/src/OTP.php index 7e68dfc..e042917 100644 --- a/src/OTP.php +++ b/src/OTP.php @@ -4,14 +4,14 @@ namespace OTPHP; -use function assert; -use function chr; -use function count; use Exception; use InvalidArgumentException; -use function is_string; use ParagonIE\ConstantTime\Base32; use RuntimeException; +use function assert; +use function chr; +use function count; +use function is_string; use const STR_PAD_LEFT; abstract class OTP implements OTPInterface diff --git a/src/ParameterTrait.php b/src/ParameterTrait.php index 3b2641e..dc92861 100644 --- a/src/ParameterTrait.php +++ b/src/ParameterTrait.php @@ -4,10 +4,10 @@ namespace OTPHP; +use InvalidArgumentException; use function array_key_exists; use function assert; use function in_array; -use InvalidArgumentException; use function is_int; use function is_string; diff --git a/src/TOTP.php b/src/TOTP.php index 3a7d728..8a1cfeb 100644 --- a/src/TOTP.php +++ b/src/TOTP.php @@ -4,8 +4,8 @@ namespace OTPHP; -use function assert; use InvalidArgumentException; +use function assert; use function is_int; /** @@ -133,23 +133,21 @@ public function setEpoch(int $epoch): void */ protected function getParameterMap(): array { - return array_merge( - parent::getParameterMap(), - [ - 'period' => static function ($value): int { - (int) $value > 0 || throw new InvalidArgumentException('Period must be at least 1.'); - - return (int) $value; - }, - 'epoch' => static function ($value): int { - (int) $value >= 0 || throw new InvalidArgumentException( - 'Epoch must be greater than or equal to 0.' - ); - - return (int) $value; - }, - ] - ); + return [ + ...parent::getParameterMap(), + 'period' => static function ($value): int { + (int) $value > 0 || throw new InvalidArgumentException('Period must be at least 1.'); + + return (int) $value; + }, + 'epoch' => static function ($value): int { + (int) $value >= 0 || throw new InvalidArgumentException( + 'Epoch must be greater than or equal to 0.' + ); + + return (int) $value; + }, + ]; } /** diff --git a/src/Url.php b/src/Url.php index 76919d2..a97ca68 100644 --- a/src/Url.php +++ b/src/Url.php @@ -4,8 +4,8 @@ namespace OTPHP; -use function array_key_exists; use InvalidArgumentException; +use function array_key_exists; use function is_string; /** diff --git a/tests/FactoryTest.php b/tests/FactoryTest.php index dea4e03..e069bc3 100644 --- a/tests/FactoryTest.php +++ b/tests/FactoryTest.php @@ -8,6 +8,7 @@ use OTPHP\Factory; use OTPHP\HOTP; use OTPHP\TOTP; +use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; /** @@ -15,9 +16,7 @@ */ final class FactoryTest extends TestCase { - /** - * @test - */ + #[Test] public function tOTPLoad(): void { $otp = 'otpauth://totp/My%20Project%3Aalice%40foo.bar?algorithm=sha512&digits=8&foo=bar.baz&issuer=My%20Project&period=20&secret=JDDK4U6G3BJLEZ7Y'; @@ -36,9 +35,7 @@ public function tOTPLoad(): void static::assertSame($otp, $result->getProvisioningUri()); } - /** - * @test - */ + #[Test] public function tOTPObjectDoesNotHaveRequestedParameter(): void { $this->expectException(InvalidArgumentException::class); @@ -49,9 +46,7 @@ public function tOTPObjectDoesNotHaveRequestedParameter(): void $result->getParameter('image'); } - /** - * @test - */ + #[Test] public function hOTPLoad(): void { $otp = 'otpauth://hotp/My%20Project%3Aalice%40foo.bar?counter=1000&digits=8&image=https%3A%2F%2Ffoo.bar%2Fbaz&issuer=My%20Project&secret=JDDK4U6G3BJLEZ7Y'; @@ -69,9 +64,7 @@ public function hOTPLoad(): void static::assertSame($otp, $result->getProvisioningUri()); } - /** - * @test - */ + #[Test] public function badProvisioningUri1(): void { $this->expectException(InvalidArgumentException::class); @@ -80,9 +73,7 @@ public function badProvisioningUri1(): void Factory::loadFromProvisioningUri($otp); } - /** - * @test - */ + #[Test] public function badProvisioningUri2(): void { $this->expectException(InvalidArgumentException::class); @@ -91,9 +82,7 @@ public function badProvisioningUri2(): void Factory::loadFromProvisioningUri($otp); } - /** - * @test - */ + #[Test] public function badProvisioningUri3(): void { $this->expectException(InvalidArgumentException::class); @@ -102,9 +91,7 @@ public function badProvisioningUri3(): void Factory::loadFromProvisioningUri($otp); } - /** - * @test - */ + #[Test] public function badProvisioningUri4(): void { $this->expectException(InvalidArgumentException::class); @@ -113,9 +100,7 @@ public function badProvisioningUri4(): void Factory::loadFromProvisioningUri($otp); } - /** - * @test - */ + #[Test] public function badProvisioningUri5(): void { $this->expectException(InvalidArgumentException::class); @@ -124,9 +109,7 @@ public function badProvisioningUri5(): void Factory::loadFromProvisioningUri($otp); } - /** - * @test - */ + #[Test] public function badProvisioningUri6(): void { $this->expectException(InvalidArgumentException::class); @@ -135,9 +118,7 @@ public function badProvisioningUri6(): void Factory::loadFromProvisioningUri($otp); } - /** - * @test - */ + #[Test] public function tOTPLoadWithoutIssuer(): void { $otp = 'otpauth://totp/My%20Test%20-%20Auth?secret=JDDK4U6G3BJLEZ7Y'; @@ -154,9 +135,7 @@ public function tOTPLoadWithoutIssuer(): void static::assertSame($otp, $result->getProvisioningUri()); } - /** - * @test - */ + #[Test] public function tOTPLoadAndRemoveSecretTrailingCharacters(): void { $uri = 'otpauth://totp/My%20Test%20-%20Auth?secret=JDDK4U6G3BJLEQ%3D%3D'; diff --git a/tests/HOTPTest.php b/tests/HOTPTest.php index 06ab342..6c65caf 100644 --- a/tests/HOTPTest.php +++ b/tests/HOTPTest.php @@ -6,6 +6,7 @@ use InvalidArgumentException; use OTPHP\HOTP; +use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; use RuntimeException; @@ -14,9 +15,7 @@ */ final class HOTPTest extends TestCase { - /** - * @test - */ + #[Test] public function labelNotDefined(): void { $this->expectException(InvalidArgumentException::class); @@ -25,9 +24,7 @@ public function labelNotDefined(): void $hotp->getProvisioningUri(); } - /** - * @test - */ + #[Test] public function issuerHasColon(): void { $otp = HOTP::createFromSecret('JDDK4U6G3BJLEZ7Y'); @@ -37,9 +34,7 @@ public function issuerHasColon(): void $otp->setIssuer('foo%3Abar'); } - /** - * @test - */ + #[Test] public function issuerHasColon2(): void { $otp = HOTP::createFromSecret('JDDK4U6G3BJLEZ7Y'); @@ -49,9 +44,7 @@ public function issuerHasColon2(): void $otp->setIssuer('foo%3abar'); } - /** - * @test - */ + #[Test] public function labelHasColon(): void { $otp = HOTP::createFromSecret('JDDK4U6G3BJLEZ7Y'); @@ -61,9 +54,7 @@ public function labelHasColon(): void $otp->setLabel('foo%3Abar'); } - /** - * @test - */ + #[Test] public function labelHasColon2(): void { $otp = HOTP::createFromSecret('JDDK4U6G3BJLEZ7Y'); @@ -73,9 +64,7 @@ public function labelHasColon2(): void $otp->setLabel('foo:bar'); } - /** - * @test - */ + #[Test] public function digitsIsNot1OrMore(): void { $htop = HOTP::createFromSecret('JDDK4U6G3BJLEZ7Y'); @@ -85,9 +74,7 @@ public function digitsIsNot1OrMore(): void $htop->setDigits(0); } - /** - * @test - */ + #[Test] public function counterIsNot1OrMore(): void { $htop = HOTP::createFromSecret('JDDK4U6G3BJLEZ7Y'); @@ -97,9 +84,7 @@ public function counterIsNot1OrMore(): void $htop->setCounter(-500); } - /** - * @test - */ + #[Test] public function digestIsNotSupported(): void { $htop = HOTP::createFromSecret('JDDK4U6G3BJLEZ7Y'); @@ -111,9 +96,8 @@ public function digestIsNotSupported(): void /** * xpectedExceptionMessage. - * - * @test */ + #[Test] public function secretShouldBeBase32Encoded(): void { $otp = HOTP::createFromSecret(random_bytes(32)); @@ -123,9 +107,7 @@ public function secretShouldBeBase32Encoded(): void $otp->at(0); } - /** - * @test - */ + #[Test] public function objectCreationValid(): void { $otp = HOTP::generate(); @@ -133,9 +115,7 @@ public function objectCreationValid(): void static::assertMatchesRegularExpression('/^[A-Z2-7]+$/', $otp->getSecret()); } - /** - * @test - */ + #[Test] public function getProvisioningUri(): void { $otp = $this->createHOTP(8, 'sha1', 1000); @@ -147,9 +127,7 @@ public function getProvisioningUri(): void ); } - /** - * @test - */ + #[Test] public function verifyCounterInvalid(): void { $otp = $this->createHOTP(8, 'sha1', 1000); @@ -157,9 +135,7 @@ public function verifyCounterInvalid(): void static::assertFalse($otp->verify('98449994', 100)); } - /** - * @test - */ + #[Test] public function verifyCounterChanged(): void { $otp = $this->createHOTP(8, 'sha1', 1100); @@ -169,9 +145,7 @@ public function verifyCounterChanged(): void static::assertSame($otp->getCounter(), 1101); } - /** - * @test - */ + #[Test] public function verifyValidInWindow(): void { $otp = $this->createHOTP(8, 'sha1', 1000); diff --git a/tests/TOTPTest.php b/tests/TOTPTest.php index c868a75..3deeae9 100644 --- a/tests/TOTPTest.php +++ b/tests/TOTPTest.php @@ -4,23 +4,24 @@ namespace OTPHP\Test; -use function assert; use InvalidArgumentException; +use Iterator; use OTPHP\TOTP; use OTPHP\TOTPInterface; use ParagonIE\ConstantTime\Base32; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; use RuntimeException; use Symfony\Bridge\PhpUnit\ClockMock; +use function assert; /** * @internal */ final class TOTPTest extends TestCase { - /** - * @test - */ + #[Test] public function labelNotDefined(): void { $this->expectException(InvalidArgumentException::class); @@ -29,9 +30,7 @@ public function labelNotDefined(): void $otp->getProvisioningUri(); } - /** - * @test - */ + #[Test] public function customParameter(): void { $otp = TOTP::createFromSecret('JDDK4U6G3BJLEZ7Y'); @@ -49,9 +48,7 @@ public function customParameter(): void ); } - /** - * @test - */ + #[Test] public function objectCreationValid(): void { $otp = TOTP::generate(); @@ -59,9 +56,7 @@ public function objectCreationValid(): void static::assertMatchesRegularExpression('/^[A-Z2-7]+$/', $otp->getSecret()); } - /** - * @test - */ + #[Test] public function periodIsNot1OrMore(): void { $totp = TOTP::createFromSecret('JDDK4U6G3BJLEZ7Y'); @@ -71,9 +66,7 @@ public function periodIsNot1OrMore(): void $totp->setPeriod(-20); } - /** - * @test - */ + #[Test] public function epochIsNot0OrMore(): void { $totp = TOTP::createFromSecret('JDDK4U6G3BJLEZ7Y'); @@ -83,9 +76,7 @@ public function epochIsNot0OrMore(): void $totp->setEpoch(-1); } - /** - * @test - */ + #[Test] public function secretShouldBeBase32Encoded(): void { $this->expectException(RuntimeException::class); @@ -96,12 +87,10 @@ public function secretShouldBeBase32Encoded(): void $otp->now(); } - /** - * @test - */ + #[Test] public function getProvisioningUri(): void { - $otp = $this->createTOTP(6, 'sha1', 30); + $otp = self::createTOTP(6, 'sha1', 30); static::assertSame( 'otpauth://totp/My%20Project%3Aalice%40foo.bar?issuer=My%20Project&secret=JDDK4U6G3BJLEZ7Y', @@ -109,9 +98,7 @@ public function getProvisioningUri(): void ); } - /** - * @test - */ + #[Test] public function getProvisioningUriWithNonDefaultArgSeperator(): void { $otp = self::createTOTP(6, 'sha1', 30); @@ -128,48 +115,42 @@ public function getProvisioningUriWithNonDefaultArgSeperator(): void * @param positive-int $timestamp * @param positive-int $period * @param positive-int $expectedRemainder - * @test - * @dataProvider dataRemainingTimeBeforeExpiration */ + #[DataProvider('dataRemainingTimeBeforeExpiration')] + #[Test] public function getRemainingTimeBeforeExpiration(int $timestamp, int $period, int $expectedRemainder): void { ClockMock::register(TOTP::class); ClockMock::withClockMock($timestamp); - $otp = $this->createTOTP(6, 'sha1', $period); + $otp = self::createTOTP(6, 'sha1', $period); static::assertSame($expectedRemainder, $otp->expiresIn()); } - /** - * @test - */ + #[Test] public function generateOtpAt(): void { - $otp = $this->createTOTP(6, 'sha1', 30); + $otp = self::createTOTP(6, 'sha1', 30); static::assertSame('855783', $otp->at(0)); static::assertSame('762124', $otp->at(319690800)); static::assertSame('139664', $otp->at(1301012137)); } - /** - * @test - */ + #[Test] public function generateOtpWithEpochAt(): void { - $otp = $this->createTOTP(6, 'sha1', 30, 'JDDK4U6G3BJLEZ7Y', 'alice@foo.bar', 'My Project', 100); + $otp = self::createTOTP(6, 'sha1', 30, 'JDDK4U6G3BJLEZ7Y', 'alice@foo.bar', 'My Project', 100); static::assertSame('855783', $otp->at(100)); static::assertSame('762124', $otp->at(319690900)); static::assertSame('139664', $otp->at(1301012237)); } - /** - * @test - */ + #[Test] public function wrongSizeOtp(): void { - $otp = $this->createTOTP(6, 'sha1', 30); + $otp = self::createTOTP(6, 'sha1', 30); static::assertFalse($otp->verify('0')); static::assertFalse($otp->verify('00')); @@ -178,39 +159,33 @@ public function wrongSizeOtp(): void static::assertFalse($otp->verify('00000')); } - /** - * @test - */ + #[Test] public function generateOtpNow(): void { ClockMock::register(TOTP::class); $time = time(); ClockMock::withClockMock($time); - $otp = $this->createTOTP(6, 'sha1', 30); + $otp = self::createTOTP(6, 'sha1', 30); static::assertSame($otp->now(), $otp->at($time)); } - /** - * @test - */ + #[Test] public function verifyOtpNow(): void { ClockMock::register(TOTP::class); $time = time(); ClockMock::withClockMock($time); - $otp = $this->createTOTP(6, 'sha1', 30); + $otp = self::createTOTP(6, 'sha1', 30); $totp = $otp->at($time); static::assertTrue($otp->verify($totp, $time)); } - /** - * @test - */ + #[Test] public function verifyOtp(): void { - $otp = $this->createTOTP(6, 'sha1', 30); + $otp = self::createTOTP(6, 'sha1', 30); static::assertTrue($otp->verify('855783', 0)); static::assertTrue($otp->verify('762124', 319690800)); @@ -221,12 +196,10 @@ public function verifyOtp(): void static::assertFalse($otp->verify('139664', 1301012197)); } - /** - * @test - */ + #[Test] public function verifyOtpWithEpoch(): void { - $otp = $this->createTOTP(6, 'sha1', 30, 'JDDK4U6G3BJLEZ7Y', 'alice@foo.bar', 'My Project', 100); + $otp = self::createTOTP(6, 'sha1', 30, 'JDDK4U6G3BJLEZ7Y', 'alice@foo.bar', 'My Project', 100); static::assertTrue($otp->verify('855783', 100)); static::assertTrue($otp->verify('762124', 319690900)); @@ -237,12 +210,10 @@ public function verifyOtpWithEpoch(): void static::assertFalse($otp->verify('139664', 1301012297)); } - /** - * @test - */ + #[Test] public function notCompatibleWithGoogleAuthenticator(): void { - $otp = $this->createTOTP(9, 'sha512', 10); + $otp = self::createTOTP(9, 'sha512', 10); static::assertSame( 'otpauth://totp/My%20Project%3Aalice%40foo.bar?algorithm=sha512&digits=9&issuer=My%20Project&period=10&secret=JDDK4U6G3BJLEZ7Y', @@ -251,14 +222,12 @@ public function notCompatibleWithGoogleAuthenticator(): void } /** - * @dataProvider dataVectors - * * @param TOTPInterface $totp * @param positive-int $timestamp * @param non-empty-string $expected_value - * - * @test */ + #[DataProvider('dataVectors')] + #[Test] public function vectors($totp, $timestamp, $expected_value): void { static::assertSame($expected_value, $totp->at($timestamp)); @@ -268,51 +237,44 @@ public function vectors($totp, $timestamp, $expected_value): void /** * @see https://tools.ietf.org/html/rfc6238#appendix-B * @see http://www.rfc-editor.org/errata_search.php?rfc=6238 - * - * @return array */ - public function dataVectors(): array + public static function dataVectors(): Iterator { $sha1key = Base32::encodeUpper('12345678901234567890'); assert($sha1key !== ''); - $totp_sha1 = $this->createTOTP(8, 'sha1', 30, $sha1key); + $totp_sha1 = self::createTOTP(8, 'sha1', 30, $sha1key); $sha256key = Base32::encodeUpper('12345678901234567890123456789012'); assert($sha256key !== ''); - $totp_sha256 = $this->createTOTP(8, 'sha256', 30, $sha256key); + $totp_sha256 = self::createTOTP(8, 'sha256', 30, $sha256key); $sha512key = Base32::encodeUpper('1234567890123456789012345678901234567890123456789012345678901234'); assert($sha512key !== ''); - $totp_sha512 = $this->createTOTP(8, 'sha512', 30, $sha512key); - - return [ - [$totp_sha1, 59, '94287082'], - [$totp_sha256, 59, '46119246'], - [$totp_sha512, 59, '90693936'], - [$totp_sha1, 1111111109, '07081804'], - [$totp_sha256, 1111111109, '68084774'], - [$totp_sha512, 1111111109, '25091201'], - [$totp_sha1, 1111111111, '14050471'], - [$totp_sha256, 1111111111, '67062674'], - [$totp_sha512, 1111111111, '99943326'], - [$totp_sha1, 1234567890, '89005924'], - [$totp_sha256, 1234567890, '91819424'], - [$totp_sha512, 1234567890, '93441116'], - [$totp_sha1, 2000000000, '69279037'], - [$totp_sha256, 2000000000, '90698825'], - [$totp_sha512, 2000000000, '38618901'], - [$totp_sha1, 20000000000, '65353130'], - [$totp_sha256, 20000000000, '77737706'], - [$totp_sha512, 20000000000, '47863826'], - ]; + $totp_sha512 = self::createTOTP(8, 'sha512', 30, $sha512key); + yield [$totp_sha1, 59, '94287082']; + yield [$totp_sha256, 59, '46119246']; + yield [$totp_sha512, 59, '90693936']; + yield [$totp_sha1, 1111111109, '07081804']; + yield [$totp_sha256, 1111111109, '68084774']; + yield [$totp_sha512, 1111111109, '25091201']; + yield [$totp_sha1, 1111111111, '14050471']; + yield [$totp_sha256, 1111111111, '67062674']; + yield [$totp_sha512, 1111111111, '99943326']; + yield [$totp_sha1, 1234567890, '89005924']; + yield [$totp_sha256, 1234567890, '91819424']; + yield [$totp_sha512, 1234567890, '93441116']; + yield [$totp_sha1, 2000000000, '69279037']; + yield [$totp_sha256, 2000000000, '90698825']; + yield [$totp_sha512, 2000000000, '38618901']; + yield [$totp_sha1, 20000000000, '65353130']; + yield [$totp_sha256, 20000000000, '77737706']; + yield [$totp_sha512, 20000000000, '47863826']; } - /** - * @test - */ + #[Test] public function invalidOtpWindow(): void { $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('The leeway must be lower than the TOTP period'); - $otp = $this->createTOTP(6, 'sha1', 30); + $otp = self::createTOTP(6, 'sha1', 30); $otp->verify('123456', null, 31); } @@ -320,14 +282,14 @@ public function invalidOtpWindow(): void * @param positive-int $timestamp * @param non-empty-string $input * @param 0|positive-int $leeway - * @test - * @dataProvider dataLeeway */ + #[DataProvider('dataLeeway')] + #[Test] public function verifyOtpInWindow(int $timestamp, string $input, int $leeway, bool $expectedResult): void { ClockMock::register(TOTP::class); ClockMock::withClockMock($timestamp); - $otp = $this->createTOTP(6, 'sha1', 30); + $otp = self::createTOTP(6, 'sha1', 30); static::assertSame($expectedResult, $otp->verify($input, null, $leeway)); } @@ -336,9 +298,9 @@ public function verifyOtpInWindow(int $timestamp, string $input, int $leeway, bo * @param positive-int $timestamp * @param non-empty-string $input * @param 0|positive-int $leeway - * @test - * @dataProvider dataLeewayWithEpoch */ + #[DataProvider('dataLeewayWithEpoch')] + #[Test] public function verifyOtpWithEpochInWindow( int $timestamp, string $input, @@ -347,37 +309,40 @@ public function verifyOtpWithEpochInWindow( ): void { ClockMock::register(TOTP::class); ClockMock::withClockMock($timestamp); - $otp = $this->createTOTP(6, 'sha1', 30, 'JDDK4U6G3BJLEZ7Y', 'alice@foo.bar', 'My Project', 100); + $otp = self::createTOTP(6, 'sha1', 30, 'JDDK4U6G3BJLEZ7Y', 'alice@foo.bar', 'My Project', 100); static::assertSame($expectedResult, $otp->verify($input, null, $leeway)); } - /** - * @return array[] - */ - public function dataLeewayWithEpoch(): array + public static function dataLeewayWithEpoch(): Iterator { - return [ - [319690889, '762124', 10, false], //Leeway of 10 seconds, **out** the period of 11sec - [319690890, '762124', 10, true], //Leeway of 10 seconds, **out** the period of 10sec - [319690899, '762124', 10, true], //Leeway of 10 seconds, **out** the period of 1sec - [319690899, '762124', 0, false], //No leeway, **out** the period - [319690900, '762124', 0, true], //No leeway, in the period - [319690920, '762124', 0, true], //No leeway, in the period - [319690929, '762124', 0, true], //No leeway, in the period - [319690930, '762124', 0, false], //No leeway, **out** the period - [319690930, '762124', 10, true], //Leeway of 10 seconds, **out** the period of 1sec - [319690939, '762124', 10, true], //Leeway of 10 seconds, **out** the period of 10sec - [319690940, '762124', 10, false], //Leeway of 10 seconds, **out** the period of 11sec - ]; + yield [319690889, '762124', 10, false]; + //Leeway of 10 seconds, **out** the period of 11sec + yield [319690890, '762124', 10, true]; + //Leeway of 10 seconds, **out** the period of 10sec + yield [319690899, '762124', 10, true]; + //Leeway of 10 seconds, **out** the period of 1sec + yield [319690899, '762124', 0, false]; + //No leeway, **out** the period + yield [319690900, '762124', 0, true]; + //No leeway, in the period + yield [319690920, '762124', 0, true]; + //No leeway, in the period + yield [319690929, '762124', 0, true]; + //No leeway, in the period + yield [319690930, '762124', 0, false]; + //No leeway, **out** the period + yield [319690930, '762124', 10, true]; + //Leeway of 10 seconds, **out** the period of 1sec + yield [319690939, '762124', 10, true]; + //Leeway of 10 seconds, **out** the period of 10sec + yield [319690940, '762124', 10, false]; } - /** - * @test - */ + #[Test] public function qRCodeUri(): void { - $otp = $this->createTOTP(6, 'sha1', 30, 'DJBSWY3DPEHPK3PXP', 'alice@google.com', 'My Big Compagny'); + $otp = self::createTOTP(6, 'sha1', 30, 'DJBSWY3DPEHPK3PXP', 'alice@google.com', 'My Big Compagny'); static::assertSame( 'http://chart.apis.google.com/chart?cht=qr&chs=250x250&chl=otpauth%3A%2F%2Ftotp%2FMy%2520Big%2520Compagny%253Aalice%2540google.com%3Fissuer%3DMy%2520Big%2520Compagny%26secret%3DDJBSWY3DPEHPK3PXP', @@ -395,45 +360,45 @@ public function qRCodeUri(): void ); } - /** - * @return int[][] - */ - public function dataRemainingTimeBeforeExpiration(): array + public static function dataRemainingTimeBeforeExpiration(): Iterator { - return [ - [1644926810, 90, 40], - [1644926810, 30, 10], - [1644926810, 20, 10], - [1577833199, 90, 1], - [1577833199, 30, 1], - [1577833199, 20, 1], - [1577833200, 90, 90], - [1577833200, 30, 30], - [1577833200, 20, 20], - [1577833201, 90, 89], - [1577833201, 30, 29], - [1577833201, 20, 19], - ]; + yield [1644926810, 90, 40]; + yield [1644926810, 30, 10]; + yield [1644926810, 20, 10]; + yield [1577833199, 90, 1]; + yield [1577833199, 30, 1]; + yield [1577833199, 20, 1]; + yield [1577833200, 90, 90]; + yield [1577833200, 30, 30]; + yield [1577833200, 20, 20]; + yield [1577833201, 90, 89]; + yield [1577833201, 30, 29]; + yield [1577833201, 20, 19]; } - /** - * @return array[] - */ - public function dataLeeway(): array + public static function dataLeeway(): Iterator { - return [ - [319690789, '762124', 10, false], //Leeway of 10 seconds, **out** the period of 11sec - [319690790, '762124', 10, true], //Leeway of 10 seconds, **out** the period of 10sec - [319690799, '762124', 10, true], //Leeway of 10 seconds, **out** the period of 1sec - [319690799, '762124', 0, false], //No leeway, **out** the period - [319690800, '762124', 0, true], //No leeway, in the period - [319690820, '762124', 0, true], //No leeway, in the period - [319690829, '762124', 0, true], //No leeway, in the period - [319690830, '762124', 0, false], //No leeway, **out** the period - [319690830, '762124', 10, true], //Leeway of 10 seconds, **out** the period of 1sec - [319690839, '762124', 10, true], //Leeway of 10 seconds, **out** the period of 10sec - [319690840, '762124', 10, false], //Leeway of 10 seconds, **out** the period of 11sec - ]; + yield [319690789, '762124', 10, false]; + //Leeway of 10 seconds, **out** the period of 11sec + yield [319690790, '762124', 10, true]; + //Leeway of 10 seconds, **out** the period of 10sec + yield [319690799, '762124', 10, true]; + //Leeway of 10 seconds, **out** the period of 1sec + yield [319690799, '762124', 0, false]; + //No leeway, **out** the period + yield [319690800, '762124', 0, true]; + //No leeway, in the period + yield [319690820, '762124', 0, true]; + //No leeway, in the period + yield [319690829, '762124', 0, true]; + //No leeway, in the period + yield [319690830, '762124', 0, false]; + //No leeway, **out** the period + yield [319690830, '762124', 10, true]; + //Leeway of 10 seconds, **out** the period of 1sec + yield [319690839, '762124', 10, true]; + //Leeway of 10 seconds, **out** the period of 10sec + yield [319690840, '762124', 10, false]; } /** @@ -445,7 +410,7 @@ public function dataLeeway(): array * @param non-empty-string $issuer * @param 0|positive-int $epoch */ - private function createTOTP( + private static function createTOTP( int $digits, string $digest, int $period,