Skip to content

Commit

Permalink
Feature/psr 20 (#187)
Browse files Browse the repository at this point in the history
* PSR-20
* PHPUnit configuration migration
* CI/CD fixed
  • Loading branch information
Spomky authored Apr 15, 2023
1 parent 9a15690 commit ce15080
Show file tree
Hide file tree
Showing 15 changed files with 313 additions and 314 deletions.
6 changes: 3 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

.PHONY: mu
mu: vendor ## Mutation tests
vendor/bin/infection -s --threads=$$(nproc) --min-msi=30 --min-covered-msi=50
XDEBUG_MODE=coverage vendor/bin/infection -s --threads=$$(nproc) --min-msi=30 --min-covered-msi=50

.PHONY: tests
tests: vendor ## Run all tests
Expand Down Expand Up @@ -42,11 +42,11 @@ st: vendor ## Run static analyse

.PHONY: ci-mu
ci-mu: vendor ## Mutation tests (for CI/CD only)
vendor/bin/infection --logger-github -s --threads=$$(nproc) --min-msi=30 --min-covered-msi=50
XDEBUG_MODE=coverage vendor/bin/infection --logger-github -s --threads=$$(nproc) --min-msi=30 --min-covered-msi=50

.PHONY: ci-cc
ci-cc: vendor ## Show test coverage rates (for CI/CD only)
vendor/bin/phpunit --coverage-text
XDEBUG_MODE=coverage vendor/bin/phpunit --coverage-text

.PHONY: ci-cs
ci-cs: vendor ## Check all files using defined ECS rules (for CI/CD only)
Expand Down
11 changes: 6 additions & 5 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,22 +16,23 @@
}
],
"require": {
"php": "^8.1",
"php": ">=8.1",
"ext-mbstring": "*",
"paragonie/constant_time_encoding": "^2.0"
"paragonie/constant_time_encoding": "^2.0",
"psr/clock": "^1.0",
"symfony/deprecation-contracts": "^3.2"
},
"require-dev": {
"ekino/phpstan-banned-code": "^1.0",
"infection/infection": "^0.26",
"infection/infection": "^0.26.20",
"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": "^10.1",
"qossmic/deptrac-shim": "^1.0",
"rector/rector": "^0.15",
"symfony/phpunit-bridge": "^6.1",
"symplify/easy-coding-standard": "^11.0"
},
"autoload": {
Expand Down
1 change: 1 addition & 0 deletions deptrac.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ parameters:
- name: 'Vendors'
collectors:
- { type: className, regex: '^ParagonIE\\' }
- { type: className, regex: '^Psr\\Clock\\' }
ruleset:
OTP:
- 'Vendors'
37 changes: 21 additions & 16 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,6 @@ parameters:
count: 1
path: src/HOTP.php

-
message: "#^Comparison operation \"\\>\\=\" between int\\<0, max\\>\\|null and 0 is always true\\.$#"
count: 1
path: src/HOTP.php

-
message: "#^Method OTPHP\\\\OTP\\:\\:generateSecret\\(\\) should return non\\-empty\\-string but returns string\\.$#"
count: 1
Expand All @@ -20,11 +15,6 @@ parameters:
count: 1
path: src/OTP.php

-
message: "#^Comparison operation \"\\>\\=\" between int\\<0, max\\> and 0 is always true\\.$#"
count: 1
path: src/TOTP.php

-
message: "#^Method OTPHP\\\\TOTP\\:\\:expiresIn\\(\\) should return int\\<0, max\\> but returns int\\.$#"
count: 1
Expand All @@ -37,14 +27,9 @@ parameters:

-
message: "#^Parameter \\#1 \\$input of method OTPHP\\\\TOTP\\:\\:at\\(\\) expects int\\<0, max\\>, int given\\.$#"
count: 1
count: 2
path: src/TOTP.php

-
message: "#^Left side of \\|\\| is always true\\.$#"
count: 1
path: src/Url.php

-
message: "#^Parameter \\#2 \\$host of class OTPHP\\\\Url constructor expects non\\-empty\\-string, string given\\.$#"
count: 1
Expand Down Expand Up @@ -75,12 +60,32 @@ parameters:
count: 1
path: tests/HOTPTest.php

-
message: "#^Parameter \\#1 \\$dateTime of method OTPHP\\\\Test\\\\ClockMock\\:\\:setDateTime\\(\\) expects DateTimeImmutable\\|null, DateTimeImmutable\\|false given\\.$#"
count: 5
path: tests/TOTPTest.php

-
message: "#^Parameter \\#1 \\$epoch of method OTPHP\\\\TOTP\\:\\:setEpoch\\(\\) expects int\\<0, max\\>, \\-1 given\\.$#"
count: 1
path: tests/TOTPTest.php

-
message: "#^Parameter \\#1 \\$otp of method OTPHP\\\\TOTP\\:\\:verify\\(\\) expects non\\-empty\\-string, string given\\.$#"
count: 2
path: tests/TOTPTest.php

-
message: "#^Parameter \\#1 \\$period of method OTPHP\\\\TOTP\\:\\:setPeriod\\(\\) expects int\\<1, max\\>, \\-20 given\\.$#"
count: 1
path: tests/TOTPTest.php

-
message: "#^Parameter \\#3 \\$leeway of method OTPHP\\\\TOTP\\:\\:verify\\(\\) expects int\\<0, max\\>\\|null, int given\\.$#"
count: 2
path: tests/TOTPTest.php

-
message: "#^Parameter \\#3 \\$period of static method OTPHP\\\\Test\\\\TOTPTest\\:\\:createTOTP\\(\\) expects int\\<1, max\\>, int given\\.$#"
count: 1
path: tests/TOTPTest.php
1 change: 1 addition & 0 deletions phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ parameters:
- src
- tests

treatPhpDocTypesAsCertain: false
includes:
- vendor/phpstan/phpstan/conf/bleedingEdge.neon
- vendor/phpstan/phpstan-strict-rules/rules.neon
Expand Down
28 changes: 7 additions & 21 deletions phpunit.xml.dist
Original file line number Diff line number Diff line change
@@ -1,28 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit
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"
>
<coverage>
<include>
<directory suffix=".php">./src</directory>
</include>
</coverage>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.1/phpunit.xsd" bootstrap="vendor/autoload.php">
<testsuites>
<testsuite name="OTP Test Suite">
<directory>./tests</directory>
</testsuite>
</testsuites>
<listeners>
<listener class="Symfony\Bridge\PhpUnit\SymfonyTestsListener">
<arguments>
<array>
<element key="time-sensitive"><string>OTPHP\TOTP</string></element>
</array>
</arguments>
</listener>
</listeners>
<coverage/>
<source>
<include>
<directory>./src</directory>
</include>
</source>
</phpunit>
5 changes: 4 additions & 1 deletion rector.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@
PHPUnitSetList::PHPUNIT_YIELD_DATA_PROVIDER,
]);
$config->parallel();
$config->paths([__DIR__ . '/src']);
$config->paths([
__DIR__ . '/src',
__DIR__ . '/tests',
]);
$config->phpVersion(PhpVersion::PHP_81);
$config->importNames();
$config->importShortClasses();
Expand Down
17 changes: 13 additions & 4 deletions src/Factory.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use function assert;
use function count;
use InvalidArgumentException;
use Psr\Clock\ClockInterface;
use Throwable;

/**
Expand All @@ -16,16 +17,24 @@
*/
final class Factory implements FactoryInterface
{
public static function loadFromProvisioningUri(string $uri): OTPInterface
public static function loadFromProvisioningUri(string $uri, ?ClockInterface $clock = null): OTPInterface
{
try {
$parsed_url = Url::fromString($uri);
$parsed_url->getScheme() === 'otpauth' || throw new InvalidArgumentException('Invalid scheme.');
} catch (Throwable $throwable) {
throw new InvalidArgumentException('Not a valid OTP provisioning URI', $throwable->getCode(), $throwable);
}
if ($clock === null) {
trigger_deprecation(
'spomky-labs/otphp',
'11.3.0',
'The parameter "$clock" will become mandatory in 12.0.0. Please set a valid PSR Clock implementation instead of "null".'
);
$clock = new InternalClock();
}

$otp = self::createOTP($parsed_url);
$otp = self::createOTP($parsed_url, $clock);

self::populateOTP($otp, $parsed_url);

Expand Down Expand Up @@ -62,11 +71,11 @@ private static function populateOTP(OTPInterface $otp, Url $data): void
$otp->setIssuer($result[0]);
}

private static function createOTP(Url $parsed_url): OTPInterface
private static function createOTP(Url $parsed_url, ClockInterface $clock): OTPInterface
{
switch ($parsed_url->getHost()) {
case 'totp':
$totp = TOTP::createFromSecret($parsed_url->getSecret());
$totp = TOTP::createFromSecret($parsed_url->getSecret(), $clock);
$totp->setLabel(self::getLabel($parsed_url->getPath()));

return $totp;
Expand Down
19 changes: 19 additions & 0 deletions src/InternalClock.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

declare(strict_types=1);

namespace OTPHP;

use DateTimeImmutable;
use Psr\Clock\ClockInterface;

/**
* @internal
*/
final class InternalClock implements ClockInterface
{
public function now(): DateTimeImmutable
{
return new DateTimeImmutable();
}
}
40 changes: 30 additions & 10 deletions src/TOTP.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,41 @@
use function assert;
use InvalidArgumentException;
use function is_int;
use Psr\Clock\ClockInterface;

/**
* @see \OTPHP\Test\TOTPTest
*/
final class TOTP extends OTP implements TOTPInterface
{
private readonly ClockInterface $clock;

public function __construct(string $secret, ?ClockInterface $clock = null)
{
parent::__construct($secret);
if ($clock === null) {
trigger_deprecation(
'spomky-labs/otphp',
'11.3.0',
'The parameter "$clock" will become mandatory in 12.0.0. Please set a valid PSR Clock implementation instead of "null".'
);
$clock = new InternalClock();
}

$this->clock = $clock;
}

public static function create(
null|string $secret = null,
int $period = self::DEFAULT_PERIOD,
string $digest = self::DEFAULT_DIGEST,
int $digits = self::DEFAULT_DIGITS,
int $epoch = self::DEFAULT_EPOCH
int $epoch = self::DEFAULT_EPOCH,
?ClockInterface $clock = null
): self {
$totp = $secret !== null
? self::createFromSecret($secret)
: self::generate()
? self::createFromSecret($secret, $clock)
: self::generate($clock)
;
$totp->setPeriod($period);
$totp->setDigest($digest);
Expand All @@ -32,9 +51,9 @@ public static function create(
return $totp;
}

public static function createFromSecret(string $secret): self
public static function createFromSecret(string $secret, ?ClockInterface $clock = null): self
{
$totp = new self($secret);
$totp = new self($secret, $clock);
$totp->setPeriod(self::DEFAULT_PERIOD);
$totp->setDigest(self::DEFAULT_DIGEST);
$totp->setDigits(self::DEFAULT_DIGITS);
Expand All @@ -43,9 +62,9 @@ public static function createFromSecret(string $secret): self
return $totp;
}

public static function generate(): self
public static function generate(?ClockInterface $clock = null): self
{
return self::createFromSecret(self::generateSecret());
return self::createFromSecret(self::generateSecret(), $clock);
}

public function getPeriod(): int
Expand All @@ -68,7 +87,7 @@ public function expiresIn(): int
{
$period = $this->getPeriod();

return $period - (time() % $this->getPeriod());
return $period - ($this->clock->now()->getTimestamp() % $this->getPeriod());
}

public function at(int $input): string
Expand All @@ -78,7 +97,7 @@ public function at(int $input): string

public function now(): string
{
return $this->at(time());
return $this->at($this->clock->now()->getTimestamp());
}

/**
Expand All @@ -87,7 +106,8 @@ public function now(): string
*/
public function verify(string $otp, null|int $timestamp = null, null|int $leeway = null): bool
{
$timestamp ??= time();
$timestamp ??= $this->clock->now()
->getTimestamp();
$timestamp >= 0 || throw new InvalidArgumentException('Timestamp must be at least 0.');

if ($leeway === null) {
Expand Down
1 change: 0 additions & 1 deletion src/Url.php
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,6 @@ public static function fromString(string $uri): self
array_key_exists($key, $parsed_url) || throw new InvalidArgumentException(
'Not a valid OTP provisioning URI'
);
is_string($parsed_url[$key]) || throw new InvalidArgumentException('Not a valid OTP provisioning URI');
}
$scheme = $parsed_url['scheme'] ?? null;
$host = $parsed_url['host'] ?? null;
Expand Down
26 changes: 26 additions & 0 deletions tests/ClockMock.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

declare(strict_types=1);

namespace OTPHP\Test;

use DateTimeImmutable;
use Psr\Clock\ClockInterface;

/**
* @internal
*/
final class ClockMock implements ClockInterface
{
private ?DateTimeImmutable $dateTime = null;

public function now(): DateTimeImmutable
{
return $this->dateTime ?? new DateTimeImmutable();
}

public function setDateTime(?DateTimeImmutable $dateTime): void
{
$this->dateTime = $dateTime;
}
}
Loading

0 comments on commit ce15080

Please sign in to comment.