First bunch of unit tests and resulting fixes.

This commit is contained in:
flash 2025-05-14 13:58:52 +02:00
parent 78ca2dd426
commit 3fbbdd5b9a
Signed by: flash
GPG key ID: 6D833BE0D210AC02
30 changed files with 1855 additions and 13252 deletions

View file

@ -11,6 +11,9 @@ DEV_TARGETS := install update analyse analyze
DEV_DIR := $(realpath dev)
DEV_DIRS := $(wildcard ${DEV_DIR}/*)
VENDOR := $(realpath vendor)
VENDOR_BIN := ${VENDOR}/bin
# We'll use the lowest common denominator to maintain the root packages
PHP_702 := $(shell which php7.2)
COMPOSER := $(shell which composer)
@ -28,10 +31,13 @@ install:
update:
${PHP_702} ${COMPOSER} update
tests:
${PHP_702} ${VENDOR_BIN}/phpunit
fix-perms: # WSL skill issues
${SUDO} chown -R ${USER_NAME}:${GROUP_NAME} .
${DEV_DIRS}:
${MAKE} -C $@ ${MAKECMDGOALS}
.PHONY: ${DEV_TARGETS} ${DEV_DIRS} fix-perms
.PHONY: ${DEV_TARGETS} ${DEV_DIRS} fix-perms tests

View file

@ -1 +1 @@
v0.3.1
v0.3.2

View file

@ -13,6 +13,9 @@
"guzzlehttp/guzzle": "~7.9",
"guzzlehttp/psr7": "~2.7"
},
"require-dev": {
"phpunit/phpunit": "~8.5"
},
"authors": [
{
"name": "flashwave",
@ -28,5 +31,10 @@
"files": [
"polyfill.php"
]
},
"autoload-dev": {
"classmap": [
"tests"
]
}
}

1434
composer.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,5 @@
{
"require-dev": {
"phpstan/phpstan": "^1.12",
"phpunit/phpunit": "~8.5"
"phpstan/phpstan": "^1.12"
}
}

1441
dev/php702/composer.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,5 @@
{
"require-dev": {
"phpstan/phpstan": "^1.12",
"phpunit/phpunit": "~9.6"
"phpstan/phpstan": "^1.12"
}
}

1759
dev/php703/composer.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,5 @@
{
"require-dev": {
"phpstan/phpstan": "^2.1",
"phpunit/phpunit": "~9.6"
"phpstan/phpstan": "^2.1"
}
}

1761
dev/php704/composer.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,5 @@
{
"require-dev": {
"phpstan/phpstan": "^2.1",
"phpunit/phpunit": "~9.6"
"phpstan/phpstan": "^2.1"
}
}

1761
dev/php800/composer.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,5 @@
{
"require-dev": {
"phpstan/phpstan": "^2.1",
"phpunit/phpunit": "~10.5"
"phpstan/phpstan": "^2.1"
}
}

1644
dev/php801/composer.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,5 @@
{
"require-dev": {
"phpstan/phpstan": "^2.1",
"phpunit/phpunit": "~11.5"
"phpstan/phpstan": "^2.1"
}
}

1708
dev/php802/composer.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,5 @@
{
"require-dev": {
"phpstan/phpstan": "^2.1",
"phpunit/phpunit": "~12.1"
"phpstan/phpstan": "^2.1"
}
}

1602
dev/php803/composer.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,5 @@
{
"require-dev": {
"phpstan/phpstan": "^2.1",
"phpunit/phpunit": "~12.1"
"phpstan/phpstan": "^2.1"
}
}

1602
dev/php804/composer.lock generated

File diff suppressed because it is too large Load diff

View file

@ -7,6 +7,7 @@ parameters:
reportUnmatchedIgnoredErrors: false
paths:
- src
- tests
- polyfill.php
bootstrapFiles:
- vendor/autoload.php

15
phpunit.xml Normal file
View file

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/8.5/phpunit.xsd"
bootstrap="vendor/autoload.php"
beStrictAboutOutputDuringTests="true"
colors="true"
executionOrder="depends,defects"
failOnRisky="true"
failOnWarning="true">
<testsuites>
<testsuite name="default">
<directory suffix="Test.php">tests</directory>
</testsuite>
</testsuites>
</phpunit>

View file

@ -22,7 +22,7 @@ final class ClaimsUtils {
*/
public static function filterIntOrFloatToInt($value): int {
if(is_float($value))
$value = (int)floor($value);
$value = (int)$value;
elseif(!is_int($value))
throw new UnexpectedValueException('Value must be an integer or a float.');

View file

@ -41,7 +41,8 @@ final class DateTimeUtils {
return $dti;
}
$dt = DateTimeImmutable::createFromFormat('Uv', $dt->format('Uv'));
$tz = $dt->getTimezone();
$dt = DateTimeImmutable::createFromFormat('U.u', $dt->format('U.u'), $tz === false ? null : $tz);
if($dt === false)
throw new UnexpectedValueException('Date/Time representation could not be converted.');

43
tests/NoneJwkSetTest.php Normal file
View file

@ -0,0 +1,43 @@
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
use Railgun\Jwt\{NoneJwk,NoneJwkSet};
final class NoneJwkSetTest extends TestCase {
public function testJsonSerialize(): void {
$this->assertSame('{"keys":[]}', json_encode(new NoneJwkSet));
}
public function testPhpSerialize(): void {
$orig = new NoneJwkSet;
$serz = serialize($orig);
$this->assertSame("O:22:\"Railgun\Jwt\NoneJwkSet\":1:{s:27:\"\0Railgun\Jwt\NoneJwkSet\0jwk\";O:19:\"Railgun\Jwt\NoneJwk\":1:{s:23:\"\0Railgun\Jwt\NoneJwk\0id\";N;}}", $serz);
$saisei = unserialize($serz);
$this->assertInstanceOf(NoneJwkSet::class, $saisei);
}
public function testStaticValues(): void {
$jwks = new NoneJwkSet;
$this->assertSame($jwks, $jwks->getPublicKeySet());
$keys = $jwks->getKeys();
$this->assertCount(1, $keys);
$this->assertInstanceOf(NoneJwk::class, $keys[0]);
$key1 = $jwks->getKey();
$this->assertInstanceOf(NoneJwk::class, $key1);
$key2 = $jwks->getKey(null, 'none');
$this->assertInstanceOf(NoneJwk::class, $key2);
$this->assertSame($key1, $key2);
$this->assertSame($key2, $keys[0]);
}
public function testGetKeyFail(): void {
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('could not find a key that matched the requested algorithm');
(new NoneJwkSet)->getKey('the', 'ES256K');
}
}

83
tests/NoneJwkTest.php Normal file
View file

@ -0,0 +1,83 @@
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
use Railgun\Jwt\NoneJwk;
final class NoneJwkTest extends TestCase {
/**
* @return array{?string}[]
*/
public function keyIdDataProvider(): array {
return [[null], ['none'], ['test'], ['the']];
}
/**
* @dataProvider keyIdDataProvider
*/
public function testKeyId(?string $keyId): void {
$this->assertSame($keyId, (new NoneJwk($keyId))->getId());
}
/**
* @dataProvider keyIdDataProvider
*/
public function testJsonSerialize(?string $keyId): void {
$this->assertSame('null', json_encode(new NoneJwk($keyId)));
}
/**
* @return array{?string, string}[]
*/
public function phpSerializeDataProvider(): array {
return [
[null, "O:19:\"Railgun\Jwt\NoneJwk\":1:{s:23:\"\0Railgun\Jwt\NoneJwk\0id\";N;}"],
['none', "O:19:\"Railgun\Jwt\NoneJwk\":1:{s:23:\"\0Railgun\Jwt\NoneJwk\0id\";s:4:\"none\";}"],
['test', "O:19:\"Railgun\Jwt\NoneJwk\":1:{s:23:\"\0Railgun\Jwt\NoneJwk\0id\";s:4:\"test\";}"],
['the', "O:19:\"Railgun\Jwt\NoneJwk\":1:{s:23:\"\0Railgun\Jwt\NoneJwk\0id\";s:3:\"the\";}"],
];
}
/**
* @dataProvider phpSerializeDataProvider
*/
public function testPhpSerialize(?string $keyId, string $expect): void {
$orig = new NoneJwk($keyId);
$serz = serialize($orig);
$this->assertSame($expect, $serz);
$saisei = unserialize($serz);
$this->assertInstanceOf(NoneJwk::class, $saisei);
$this->assertSame($keyId, $saisei->getId());
}
public function testStaticValues(): void {
$jwk = new NoneJwk;
$this->assertSame('none', $jwk->getAlgorithm());
$this->assertSame($jwk, $jwk->getPublicKey());
}
public function testSignSuccess(): void {
$jwk = new NoneJwk;
$this->assertSame('', $jwk->sign(random_bytes(10)));
$this->assertSame('', $jwk->sign(random_bytes(10), 'none'));
}
public function testSignFail(): void {
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('$algo must be null or the string "none"');
(new NoneJwk)->sign(random_bytes(10), 'RS256');
}
public function testVerifySuccess(): void {
$jwk = new NoneJwk;
$this->assertTrue($jwk->verify(random_bytes(10), ''));
$this->assertFalse($jwk->verify(random_bytes(10), 'test'));
$this->assertFalse($jwk->verify(random_bytes(10), random_bytes(10)));
}
public function testVerifyFail(): void {
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('$algo must be null or the string "none"');
(new NoneJwk)->verify(random_bytes(10), '', 'RS256');
}
}

View file

@ -0,0 +1,74 @@
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
use Railgun\Jwt\UnexpectedValueExceptionWithPayload;
use Railgun\Jwt\Utility\ClaimsUtils;
final class ClaimsUtilsTest extends TestCase {
public function testFilterIntOrFloatToIntSuccess(): void {
$this->assertSame(20, ClaimsUtils::filterIntOrFloatToInt(20));
$this->assertSame(-34534, ClaimsUtils::filterIntOrFloatToInt(-34534));
$this->assertSame(12345, ClaimsUtils::filterIntOrFloatToInt(12345.67890));
$this->assertSame(-9876, ClaimsUtils::filterIntOrFloatToInt(-9876.54321));
}
public function testFilterIntOrFloatToIntFail(): void {
$this->expectException(UnexpectedValueException::class);
ClaimsUtils::filterIntOrFloatToInt('12345');
}
/**
* @return array{object, }[]
*/
public function ensureIdenticalAndRetrieveDataProvider(): array {
return [
[
(object)['iat' => 10000, 'nbf' => 10000.34234, 'exp' => 20000],
['iat', 'nbf'],
[ClaimsUtils::class, 'filterIntOrFloatToInt'],
10000,
],
[
(object)['iat' => 10000, 'nbf' => 11000.34234, 'exp' => 20000],
['iat', 'nbf'],
[ClaimsUtils::class, 'filterIntOrFloatToInt'],
UnexpectedValueExceptionWithPayload::class,
],
[
(object)['iat' => '10000', 'nbf' => 10000.34234, 'exp' => 20000],
['iat', 'nbf'],
[ClaimsUtils::class, 'filterIntOrFloatToInt'],
UnexpectedValueExceptionWithPayload::class, // it should get converted to this!
],
[
(object)['iat' => 12000, 'exp' => 20000],
['iat', 'nbf'],
[ClaimsUtils::class, 'filterIntOrFloatToInt'],
12000,
],
[
(object)['exp' => 20000],
['iat', 'nbf'],
[ClaimsUtils::class, 'filterIntOrFloatToInt'],
null,
],
];
}
/**
* @param string[] $claims
* @param callable(mixed, string): mixed $filter
* @param mixed $expect
* @dataProvider ensureIdenticalAndRetrieveDataProvider
*/
public function testEnsureIdenticalAndRetrieve(object $payload, array $claims, $filter, $expect): void {
$expectIsThrowable = is_string($expect) && class_exists($expect) && is_subclass_of($expect, Throwable::class);
if($expectIsThrowable)
$this->expectException($expect);
$result = ClaimsUtils::ensureIdenticalAndRetrieve($payload, $claims, $filter);
if(!$expectIsThrowable)
$this->assertSame($expect, $result);
}
}

View file

@ -0,0 +1,42 @@
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
use Railgun\Jwt\Utility\DateTimeUtils;
final class DateTimeUtilsTest extends TestCase {
public function testEnsureImmutable(): void {
// passthru
$orig = new DateTimeImmutable('now');
$imm = DateTimeUtils::ensureImmutable($orig);
$this->assertSame($orig, $imm);
// integer timestamps
$orig = time();
$imm = DateTimeUtils::ensureImmutable($orig);
$this->assertInstanceOf(DateTimeImmutable::class, $imm);
$this->assertSame(date(DATE_ATOM, $orig), $imm->format(DATE_ATOM));
// interface
$orig = new DateTime('now');
$imm = DateTimeUtils::ensureImmutable($orig);
$this->assertNotSame($orig, $imm);
$this->assertInstanceOf(DateTimeImmutable::class, $imm);
$this->assertSame($orig->format('Uu'), $imm->format('Uu'));
}
/**
* @return string[][]
*/
public function ensureImmutableArgNameExceptionDataProvider(): array {
return [['dt'], ['validAfter'], ['expiredAt']];
}
/**
* @dataProvider ensureImmutableArgNameExceptionDataProvider
*/
public function testEnsureImmutableArgNameException(string $argName): void {
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage(sprintf('$%s must be an integer or a subclass of DateTimeInterface', $argName));
DateTimeUtils::ensureImmutable(null, $argName); // @phpstan-ignore-line
}
}

View file

@ -0,0 +1,54 @@
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
use Railgun\Jwt\{
InvalidArgumentExceptionWithPayload,
RuntimeExceptionWithPayload,
UnexpectedValueExceptionWithPayload,
WithPayload
};
use Railgun\Jwt\Utility\ExceptionUtils;
final class ExceptionUtilsTest extends TestCase {
/**
* @return array{Throwable, class-string<Throwable>, object, bool}[]
*/
public function ensureWithPayloadDataProvider(): array {
$payload = new stdClass;
$payload->hello = 'world';
$other = new stdClass;
$other->goodbye = 'seeyou';
return [
[new InvalidArgumentException('test', 1234), InvalidArgumentExceptionWithPayload::class, $payload, false],
[new UnexpectedValueException('test', 5678), UnexpectedValueExceptionWithPayload::class, $payload, false],
[new RuntimeException('test', 0xF00F), RuntimeExceptionWithPayload::class, $payload, false],
[new LogicException('test', 0, new InvalidArgumentException('inner')), RuntimeExceptionWithPayload::class, $payload, false],
[new Exception('test'), RuntimeExceptionWithPayload::class, $payload, false],
[new InvalidArgumentExceptionWithPayload($payload, 'test'), InvalidArgumentExceptionWithPayload::class, $payload, true],
[new UnexpectedValueExceptionWithPayload($payload, 'test'), UnexpectedValueExceptionWithPayload::class, $payload, true],
[new RuntimeExceptionWithPayload($payload, 'test'), RuntimeExceptionWithPayload::class, $payload, true],
[new RuntimeExceptionWithPayload($other, 'test'), RuntimeExceptionWithPayload::class, $payload, false],
];
}
/**
* @param class-string<Throwable> $expect
* @dataProvider ensureWithPayloadDataProvider
*/
public function testEnsureWithPayload(Throwable $original, string $expect, object $payload, bool $expectPassthru): void {
$converted = ExceptionUtils::ensureWithPayload($payload, $original);
$this->assertInstanceOf($expect, $converted);
$this->assertSame($payload, $converted->getPayload());
$this->assertSame($original->getMessage(), $converted->getMessage());
$this->assertSame($original->getCode(), $converted->getCode());
if($expectPassthru)
$this->assertSame($original, $converted);
else {
$this->assertSame($original, $converted->getPrevious());
$this->assertSame($original->getPrevious(), $converted->getPrevious()->getPrevious());
}
}
}

View file

@ -0,0 +1,36 @@
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
use Railgun\Jwt\Utility\Json;
final class JsonTest extends TestCase {
public function testDecodeAssoc(): void {
$this->assertIsObject(Json::decode('{"test":"obj"}', false));
$this->assertIsArray(Json::decode('{"test":"array"}', true));
}
public function testEnsureDecodeThrows(): void {
$this->expectException(RuntimeException::class);
$this->expectExceptionCode(JSON_ERROR_SYNTAX);
Json::decode('beans');
}
public function testEnsureEncodeThrowsOnResource(): void {
$this->expectException(InvalidArgumentException::class);
Json::encode(fopen('php://memory', 'rb'));
}
public function testEnsureEncodeWithoutEscapedSlashes(): void {
$this->assertSame('"there/are/no/wrong/slashes"', Json::encode('there/are/no/wrong/slashes'));
}
public function testEnsureEncodeThrows(): void {
$this->expectException(RuntimeException::class);
$this->expectExceptionCode(JSON_ERROR_RECURSION);
$obj = new class { /** @var object */ public $obj; };
$obj->obj = $obj;
Json::encode($obj);
}
}