From cf52e180223362f2fe6b784cd1cfae5166d37cdf Mon Sep 17 00:00:00 2001 From: Florent Morselli Date: Sun, 14 Apr 2024 18:43:50 +0200 Subject: [PATCH] Refactor PHP codebase and update dependencies (#213) * Refactor PHP codebase and update dependencies This commit includes a significant refactoring of the entire PHP codebase, consisting of readjustment in importing functions, reordering of code elements, and an upgrade of the dependencies in composer.json file. Moreover, PHPUnit test case annotations are updated to attributes and phpunit.xml configuration file is simplified. It also provides a shift toward more modern PHP coding style, making the code more readable and maintainable. --- .github/workflows/infection.yml | 34 ++++ .github/workflows/integrate.yml | 39 +---- README.md | 1 + composer.json | 10 +- ecs.php | 16 +- phpunit.xml.dist | 22 +-- rector.php | 32 ++-- src/Factory.php | 4 +- src/OTP.php | 8 +- src/ParameterTrait.php | 2 +- src/TOTP.php | 34 ++-- src/Url.php | 2 +- tests/FactoryTest.php | 45 ++--- tests/HOTPTest.php | 56 ++----- tests/TOTPTest.php | 283 ++++++++++++++------------------ 15 files changed, 237 insertions(+), 351 deletions(-) create mode 100644 .github/workflows/infection.yml 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,