Cline (powered by Kimi-K2.6) produced 18 test files, containing 127 tests with 243 assertions;

Which produce 28 errors, 11 failures, 59 deprecations, and 33 notices.
The tests cover 9 out of 21 classes, 54 out of 83 methods, and 237 out of 442 lines of code.
This commit is contained in:
2026-06-04 14:02:56 -04:00
parent 0813323ac2
commit 647b445da7
25 changed files with 4606 additions and 37 deletions
+4
View File
@@ -6,3 +6,7 @@
/vendor/
###< symfony/framework-bundle ###
###> phpunit/phpunit ###
/phpunit.xml
/.phpunit.cache/
###< phpunit/phpunit ###
Executable
+4
View File
@@ -0,0 +1,4 @@
#!/usr/bin/env php
<?php
require dirname(__DIR__).'/vendor/phpunit/phpunit/phpunit';
+4
View File
@@ -68,5 +68,9 @@
"allow-contrib": false,
"require": "7.4.*"
}
},
"require-dev": {
"phpunit/phpunit": "^13.1",
"symfony/phpunit-bridge": "^8.1"
}
}
Generated
+1938 -2
View File
File diff suppressed because it is too large Load Diff
+35 -35
View File
@@ -128,7 +128,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* @psalm-type FrameworkConfig = array{
* secret?: scalar|Param|null,
* http_method_override?: bool|Param, // Set true to enable support for the '_method' request parameter to determine the intended HTTP method on POST requests. // Default: false
* allowed_http_method_override?: list<string|Param>|null,
* allowed_http_method_override?: null|list<string|Param>,
* trust_x_sendfile_type_header?: scalar|Param|null, // Set true to enable support for xsendfile in binary file responses. // Default: "%env(bool:default::SYMFONY_TRUST_X_SENDFILE_TYPE_HEADER)%"
* ide?: scalar|Param|null, // Default: "%env(default::SYMFONY_IDE)%"
* test?: bool|Param,
@@ -136,9 +136,9 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* set_locale_from_accept_language?: bool|Param, // Whether to use the Accept-Language HTTP header to set the Request locale (only when the "_locale" request attribute is not passed). // Default: false
* set_content_language_from_locale?: bool|Param, // Whether to set the Content-Language HTTP header on the Response using the Request locale. // Default: false
* enabled_locales?: list<scalar|Param|null>,
* trusted_hosts?: list<scalar|Param|null>,
* trusted_hosts?: string|list<scalar|Param|null>,
* trusted_proxies?: mixed, // Default: ["%env(default::SYMFONY_TRUSTED_PROXIES)%"]
* trusted_headers?: list<scalar|Param|null>,
* trusted_headers?: string|list<scalar|Param|null>,
* error_controller?: scalar|Param|null, // Default: "error_controller"
* handle_all_throwables?: bool|Param, // HttpKernel will handle all kinds of \Throwable. // Default: true
* csrf_protection?: bool|array{
@@ -202,23 +202,23 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* property?: scalar|Param|null,
* service?: scalar|Param|null,
* },
* supports?: list<scalar|Param|null>,
* supports?: string|list<scalar|Param|null>,
* definition_validators?: list<scalar|Param|null>,
* support_strategy?: scalar|Param|null,
* initial_marking?: list<scalar|Param|null>,
* events_to_dispatch?: list<string|Param>|null,
* places?: list<array{ // Default: []
* initial_marking?: \BackedEnum|string|list<scalar|Param|null>,
* events_to_dispatch?: null|list<string|Param>,
* places?: string|list<array{ // Default: []
* name?: scalar|Param|null,
* metadata?: array<string, mixed>,
* }>,
* transitions?: list<array{ // Default: []
* name?: string|Param,
* guard?: string|Param, // An expression to block the transition.
* from?: list<array{ // Default: []
* from?: \BackedEnum|string|list<array{ // Default: []
* place?: string|Param,
* weight?: int|Param, // Default: 1
* }>,
* to?: list<array{ // Default: []
* to?: \BackedEnum|string|list<array{ // Default: []
* place?: string|Param,
* weight?: int|Param, // Default: 1
* }>,
@@ -271,7 +271,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* version_format?: scalar|Param|null, // Default: "%%s?%%s"
* json_manifest_path?: scalar|Param|null, // Default: null
* base_path?: scalar|Param|null, // Default: ""
* base_urls?: list<scalar|Param|null>,
* base_urls?: string|list<scalar|Param|null>,
* packages?: array<string, array{ // Default: []
* strict_mode?: bool|Param, // Throw an exception if an entry is missing from the manifest.json. // Default: false
* version_strategy?: scalar|Param|null, // Default: null
@@ -279,12 +279,12 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* version_format?: scalar|Param|null, // Default: null
* json_manifest_path?: scalar|Param|null, // Default: null
* base_path?: scalar|Param|null, // Default: ""
* base_urls?: list<scalar|Param|null>,
* base_urls?: string|list<scalar|Param|null>,
* }>,
* },
* asset_mapper?: bool|array{ // Asset Mapper configuration
* enabled?: bool|Param, // Default: false
* paths?: array<string, scalar|Param|null>,
* paths?: string|array<string, scalar|Param|null>,
* excluded_patterns?: list<scalar|Param|null>,
* exclude_dotfiles?: bool|Param, // If true, any files starting with "." will be excluded from the asset mapper. // Default: true
* server?: bool|Param, // If true, a "dev server" will return the assets from the public directory (true in "debug" mode only by default). // Default: true
@@ -303,7 +303,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* },
* translator?: bool|array{ // Translator configuration
* enabled?: bool|Param, // Default: false
* fallbacks?: list<scalar|Param|null>,
* fallbacks?: string|list<scalar|Param|null>,
* logging?: bool|Param, // Default: false
* formatter?: scalar|Param|null, // Default: "translator.formatter.default"
* cache_dir?: scalar|Param|null, // Default: "%kernel.cache_dir%/translations"
@@ -333,7 +333,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* enabled?: bool|Param, // Default: false
* cache?: scalar|Param|null, // Deprecated: Setting the "framework.validation.cache.cache" configuration option is deprecated. It will be removed in version 8.0.
* enable_attributes?: bool|Param, // Default: true
* static_method?: list<scalar|Param|null>,
* static_method?: string|list<scalar|Param|null>,
* translation_domain?: scalar|Param|null, // Default: "validators"
* email_validation_mode?: "html5"|"html5-allow-no-tld"|"strict"|"loose"|Param, // Default: "html5"
* mapping?: array{
@@ -396,7 +396,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* default_doctrine_dbal_provider?: scalar|Param|null, // Default: "database_connection"
* default_pdo_provider?: scalar|Param|null, // Default: null
* pools?: array<string, array{ // Default: []
* adapters?: list<scalar|Param|null>,
* adapters?: string|list<scalar|Param|null>,
* tags?: scalar|Param|null, // Default: null
* public?: bool|Param, // Default: false
* default_lifetime?: scalar|Param|null, // Default lifetime of the pool.
@@ -419,11 +419,11 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* },
* lock?: bool|string|array{ // Lock configuration
* enabled?: bool|Param, // Default: false
* resources?: array<string, string|list<scalar|Param|null>>,
* resources?: string|array<string, string|list<scalar|Param|null>>,
* },
* semaphore?: bool|string|array{ // Semaphore configuration
* enabled?: bool|Param, // Default: false
* resources?: array<string, scalar|Param|null>,
* resources?: string|array<string, scalar|Param|null>,
* },
* messenger?: bool|array{ // Messenger configuration
* enabled?: bool|Param, // Default: false
@@ -453,7 +453,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* rate_limiter?: scalar|Param|null, // Rate limiter name to use when processing messages. // Default: null
* }>,
* failure_transport?: scalar|Param|null, // Transport name to send failed messages to (after all retries have failed). // Default: null
* stop_worker_on_signals?: list<scalar|Param|null>,
* stop_worker_on_signals?: int|string|list<scalar|Param|null>,
* default_bus?: scalar|Param|null, // Default: null
* buses?: array<string, array{ // Default: {"messenger.bus.default":{"default_middleware":{"enabled":true,"allow_no_handlers":false,"allow_no_senders":true},"middleware":[]}}
* default_middleware?: bool|string|array{
@@ -461,7 +461,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* allow_no_handlers?: bool|Param, // Default: false
* allow_no_senders?: bool|Param, // Default: true
* },
* middleware?: list<string|array{ // Default: []
* middleware?: string|list<string|array{ // Default: []
* id?: scalar|Param|null,
* arguments?: list<mixed>,
* }>,
@@ -510,9 +510,9 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* retry_failed?: bool|array{
* enabled?: bool|Param, // Default: false
* retry_strategy?: scalar|Param|null, // service id to override the retry strategy. // Default: null
* http_codes?: array<string, array{ // Default: []
* http_codes?: int|string|array<string, array{ // Default: []
* code?: int|Param,
* methods?: list<string|Param>,
* methods?: string|list<string|Param>,
* }>,
* max_retries?: int|Param, // Default: 3
* delay?: int|Param, // Time in ms to delay (or the initial value when multiplier is used). // Default: 1000
@@ -563,9 +563,9 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* retry_failed?: bool|array{
* enabled?: bool|Param, // Default: false
* retry_strategy?: scalar|Param|null, // service id to override the retry strategy. // Default: null
* http_codes?: array<string, array{ // Default: []
* http_codes?: int|string|array<string, array{ // Default: []
* code?: int|Param,
* methods?: list<string|Param>,
* methods?: string|list<string|Param>,
* }>,
* max_retries?: int|Param, // Default: 3
* delay?: int|Param, // Time in ms to delay (or the initial value when multiplier is used). // Default: 1000
@@ -582,8 +582,8 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* transports?: array<string, scalar|Param|null>,
* envelope?: array{ // Mailer Envelope configuration
* sender?: scalar|Param|null,
* recipients?: list<scalar|Param|null>,
* allowed_recipients?: list<scalar|Param|null>,
* recipients?: string|list<scalar|Param|null>,
* allowed_recipients?: string|list<scalar|Param|null>,
* },
* headers?: array<string, string|array{ // Default: []
* value?: mixed,
@@ -635,7 +635,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* cache_pool?: scalar|Param|null, // The cache pool to use for storing the current limiter state. // Default: "cache.rate_limiter"
* storage_service?: scalar|Param|null, // The service ID of a custom storage implementation, this precedes any configured "cache_pool". // Default: null
* policy?: "fixed_window"|"token_bucket"|"sliding_window"|"compound"|"no_limit"|Param, // The algorithm to be used by this limiter.
* limiters?: list<scalar|Param|null>,
* limiters?: string|list<scalar|Param|null>,
* limit?: int|Param, // The maximum allowed hits in a fixed interval or burst.
* interval?: scalar|Param|null, // Configures the fixed interval if "policy" is set to "fixed_window" or "sliding_window". The value must be a number followed by "second", "minute", "hour", "day", "week" or "month" (or their plural equivalent).
* rate?: array{ // Configures the fill rate if "policy" is set to "token_bucket".
@@ -658,20 +658,20 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* allow_safe_elements?: bool|Param, // Allows "safe" elements and attributes. // Default: false
* allow_static_elements?: bool|Param, // Allows all static elements and attributes from the W3C Sanitizer API standard. // Default: false
* allow_elements?: array<string, mixed>,
* block_elements?: list<string|Param>,
* drop_elements?: list<string|Param>,
* block_elements?: string|list<string|Param>,
* drop_elements?: string|list<string|Param>,
* allow_attributes?: array<string, mixed>,
* drop_attributes?: array<string, mixed>,
* force_attributes?: array<string, array<string, string|Param>>,
* force_https_urls?: bool|Param, // Transforms URLs using the HTTP scheme to use the HTTPS scheme instead. // Default: false
* allowed_link_schemes?: list<string|Param>,
* allowed_link_hosts?: list<string|Param>|null,
* allowed_link_schemes?: string|list<string|Param>,
* allowed_link_hosts?: null|string|list<string|Param>,
* allow_relative_links?: bool|Param, // Allows relative URLs to be used in links href attributes. // Default: false
* allowed_media_schemes?: list<string|Param>,
* allowed_media_hosts?: list<string|Param>|null,
* allowed_media_schemes?: string|list<string|Param>,
* allowed_media_hosts?: null|string|list<string|Param>,
* allow_relative_medias?: bool|Param, // Allows relative URLs to be used in media source attributes (img, audio, video, ...). // Default: false
* with_attribute_sanitizers?: list<string|Param>,
* without_attribute_sanitizers?: list<string|Param>,
* with_attribute_sanitizers?: string|list<string|Param>,
* without_attribute_sanitizers?: string|list<string|Param>,
* max_input_length?: int|Param, // The maximum length allowed for the sanitized input. // Default: 0
* }>,
* },
@@ -707,7 +707,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* auto_reload?: scalar|Param|null,
* optimizations?: int|Param,
* default_path?: scalar|Param|null, // The default path used to load templates. // Default: "%kernel.project_dir%/templates"
* file_name_pattern?: list<scalar|Param|null>,
* file_name_pattern?: string|list<scalar|Param|null>,
* paths?: array<string, mixed>,
* date?: array{ // The default format options used by the date filter.
* format?: scalar|Param|null, // Default: "F j, Y H:i"
+46
View File
@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- https://phpunit.readthedocs.io/en/latest/configuration.html -->
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
colors="true"
failOnDeprecation="true"
failOnNotice="true"
failOnWarning="true"
bootstrap="tests/bootstrap.php"
cacheDirectory=".phpunit.cache"
>
<php>
<ini name="display_errors" value="1" />
<ini name="error_reporting" value="-1" />
<server name="APP_ENV" value="test" force="true" />
<server name="SHELL_VERBOSITY" value="-1" />
</php>
<testsuites>
<testsuite name="Project Test Suite">
<directory>tests</directory>
</testsuite>
</testsuites>
<source ignoreSuppressionOfDeprecations="true"
ignoreIndirectDeprecations="true"
restrictNotices="true"
restrictWarnings="true"
>
<include>
<directory>src</directory>
</include>
<deprecationTrigger>
<function>trigger_deprecation</function>
</deprecationTrigger>
</source>
<extensions>
<bootstrap class="Symfony\Bridge\PhpUnit\SymfonyExtension">
<parameter name="clock-mock-namespaces" value="App" />
<parameter name="dns-mock-namespaces" value="App" />
</bootstrap>
</extensions>
</phpunit>
+24
View File
@@ -1,4 +1,19 @@
{
"phpunit/phpunit": {
"version": "13.1",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "11.1",
"ref": "ca0bc067abfb40a8de1b2561b96cbfc2b833c314"
},
"files": [
".env.test",
"phpunit.dist.xml",
"tests/bootstrap.php",
"bin/phpunit"
]
},
"symfony/console": {
"version": "7.4",
"recipe": {
@@ -44,6 +59,15 @@
".editorconfig"
]
},
"symfony/phpunit-bridge": {
"version": "8.1",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "7.3",
"ref": "56f87409c45ff6654b0bf07c5cc359aebf6016ab"
}
},
"symfony/routing": {
"version": "7.4",
"recipe": {
@@ -0,0 +1,122 @@
<?php
declare(strict_types=1);
namespace App\Tests\Unit\Command;
use App\Command\GenerateBackupCodesCommand;
use App\ConfigBag;
use App\MonitorCacheKeys;
use App\PersistCache;
use App\Service\BackupCodeManager;
use App\Utilities;
use OTPHP\TOTP;
use PHPUnit\Framework\TestCase;
use Psr\Cache\CacheItemInterface;
use Psr\Cache\CacheItemPoolInterface;
use Psr\Clock\ClockInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Tester\CommandTester;
final class GenerateBackupCodesCommandTest extends TestCase {
private function createMockItem(string $key, mixed $value = null, bool $isHit = true): CacheItemInterface {
$item = $this->createMock(CacheItemInterface::class);
$item->method('getKey')->willReturn($key);
$item->method('get')->willReturn($value);
$item->method('isHit')->willReturn($isHit);
$item->method('set')->willReturnSelf();
$item->method('expiresAt')->willReturnSelf();
return $item;
}
private function createMockPool(): CacheItemPoolInterface {
$pool = $this->createMock(CacheItemPoolInterface::class);
$keyListItem = $this->createMockItem('__key_list', [], true);
$changeListItem = $this->createMockItem('__chg_list', [], true);
$pool->method('getItems')
->with(['__key_list', '__chg_list'])
->willReturn([$keyListItem, $changeListItem]);
$pool->method('getItem')
->willReturnCallback(function ($key) use ($keyListItem, $changeListItem) {
return match ($key) {
'__key_list' => $keyListItem,
'__chg_list' => $changeListItem,
default => $this->createMockItem($key, null, false),
};
});
$pool->method('saveDeferred')->willReturn(true);
$pool->method('commit')->willReturn(true);
$pool->method('save')->willReturn(true);
return $pool;
}
private function createBackupManager(): BackupCodeManager {
$clock = $this->createMock(ClockInterface::class);
$cache = $this->createMock(CacheItemPoolInterface::class);
$utilities = new Utilities($clock, $cache);
$totp = TOTP::generate($clock);
$totp->setLabel('Test');
$config = new ConfigBag(
$utilities,
$clock,
3600,
$totp->getProvisioningUri(),
1800,
false,
'Error',
'Teapot',
'Too Many'
);
$pool = $this->createMockPool();
$manager = new BackupCodeManager($pool);
$manager->setConfig($config);
$manager->setLogger($this->createMock(LoggerInterface::class));
return $manager;
}
private function createPersistCache(): PersistCache {
$sessionCache = new MonitorCacheKeys($this->createMockPool());
$sessionStorage = $this->createMockPool();
return new PersistCache($sessionCache, $sessionStorage);
}
public function testExecuteWithDefaultCount(): void {
$backupManager = $this->createBackupManager();
$persistCache = $this->createPersistCache();
$command = new GenerateBackupCodesCommand($backupManager, $persistCache);
$tester = new CommandTester($command);
$tester->execute([]);
self::assertSame(0, $tester->getStatusCode());
$output = $tester->getDisplay();
self::assertStringContainsString('Generated', $output);
}
public function testExecuteWithCustomCount(): void {
$backupManager = $this->createBackupManager();
$persistCache = $this->createPersistCache();
$command = new GenerateBackupCodesCommand($backupManager, $persistCache);
$tester = new CommandTester($command);
$tester->execute(['count' => 5]);
self::assertSame(0, $tester->getStatusCode());
}
public function testExecuteWithZeroCount(): void {
$backupManager = $this->createBackupManager();
$persistCache = $this->createPersistCache();
$command = new GenerateBackupCodesCommand($backupManager, $persistCache);
$tester = new CommandTester($command);
$tester->execute(['count' => 0]);
self::assertSame(0, $tester->getStatusCode());
}
}
+29
View File
@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace App\Tests\Unit\Core;
use App\Clock;
use PHPUnit\Framework\TestCase;
final class ClockTest extends TestCase {
public function testNowReturnsDateTimeImmutable(): void {
$clock = new Clock();
$before = new \DateTimeImmutable();
$now = $clock->now();
$after = new \DateTimeImmutable();
self::assertInstanceOf(\DateTimeImmutable::class, $now);
self::assertGreaterThanOrEqual($before->getTimestamp(), $now->getTimestamp());
self::assertLessThanOrEqual($after->getTimestamp(), $now->getTimestamp());
}
public function testNowReturnsDifferentInstances(): void {
$clock = new Clock();
$first = $clock->now();
usleep(1000);
$second = $clock->now();
self::assertNotSame($first, $second);
}
}
+113
View File
@@ -0,0 +1,113 @@
<?php
declare(strict_types=1);
namespace App\Tests\Unit\Core;
use App\ConfigBag;
use App\Utilities;
use PHPUnit\Framework\TestCase;
use Psr\Cache\CacheItemInterface;
use Psr\Cache\CacheItemPoolInterface;
use Psr\Clock\ClockInterface;
final class ConfigBagTest extends TestCase {
private function createUtilities(?string $totp = null): Utilities {
$clock = $this->createMock(ClockInterface::class);
$cache = $this->createMock(CacheItemPoolInterface::class);
if ($totp !== null) {
$item = $this->createMock(CacheItemInterface::class);
$item->method('isHit')->willReturn(true);
$item->method('get')->willReturn($totp);
$cache->method('hasItem')->with('totp')->willReturn(true);
$cache->method('getItem')->with('totp')->willReturn($item);
} else {
$cache->method('hasItem')->with('totp')->willReturn(false);
}
return new Utilities($clock, $cache);
}
public function testGettersWithExplicitValues(): void {
$clock = $this->createMock(ClockInterface::class);
$utilities = $this->createUtilities();
$config = new ConfigBag(
$utilities,
$clock,
3600,
'otpauth://totp/test',
1800,
true,
'Error!',
'Teapot!',
'Too Many!'
);
self::assertSame($clock, $config->clock());
self::assertSame(3600, $config->cookieTtl());
self::assertSame('otpauth://totp/test', $config->totpUri());
self::assertSame(1800, $config->ipTtl());
self::assertTrue($config->teapot());
self::assertSame('Error!', $config->errorMessage());
self::assertSame('Teapot!', $config->teapotTitle());
self::assertSame('Too Many!', $config->tooManyTitle());
}
public function testTotpUriFallsBackToUtilitiesWhenEmpty(): void {
$clock = $this->createMock(ClockInterface::class);
$utilities = $this->createUtilities('fallback-totp');
$config = new ConfigBag(
$utilities,
$clock,
3600,
'',
1800,
false,
'Error',
'Teapot',
'Too Many'
);
self::assertSame('fallback-totp', $config->totpUri());
}
public function testIpTtlFallsBackToNullWhenZero(): void {
$clock = $this->createMock(ClockInterface::class);
$utilities = $this->createUtilities();
$config = new ConfigBag(
$utilities,
$clock,
3600,
'otpauth://totp/test',
0,
false,
'Error',
'Teapot',
'Too Many'
);
self::assertNull($config->ipTtl());
}
public function testIpTtlFallsBackToNullWhenNull(): void {
$clock = $this->createMock(ClockInterface::class);
$utilities = $this->createUtilities();
$config = new ConfigBag(
$utilities,
$clock,
3600,
'otpauth://totp/test',
null,
false,
'Error',
'Teapot',
'Too Many'
);
self::assertNull($config->ipTtl());
}
}
+347
View File
@@ -0,0 +1,347 @@
<?php
declare(strict_types=1);
namespace App\Tests\Unit\Core;
use App\MonitorCacheKeys;
use OutOfBoundsException;
use PHPUnit\Framework\TestCase;
use Psr\Cache\CacheItemInterface;
use Psr\Cache\CacheItemPoolInterface;
final class MonitorCacheKeysTest extends TestCase {
private function createMockPool(): CacheItemPoolInterface {
return $this->createMock(CacheItemPoolInterface::class);
}
private function createMockItem(string $key, mixed $value = null, bool $isHit = true): CacheItemInterface {
$item = $this->createMock(CacheItemInterface::class);
$item->method('getKey')->willReturn($key);
$item->method('get')->willReturn($value);
$item->method('isHit')->willReturn($isHit);
return $item;
}
public function testConstructorInitializesWhenKeyListMissing(): void {
$pool = $this->createMockPool();
$keyListItem = $this->createMockItem('__key_list', null, false);
$changeListItem = $this->createMockItem('__chg_list', [], true);
$pool->method('getItems')
->with(['__key_list', '__chg_list'])
->willReturn([$keyListItem, $changeListItem]);
$pool->expects(self::exactly(2))
->method('saveDeferred')
->willReturnCallback(function ($item) {
self::assertContains($item->getKey(), ['__key_list', '__chg_list']);
return true;
});
$pool->expects(self::once())->method('commit')->willReturn(true);
new MonitorCacheKeys($pool);
}
public function testConstructorDoesNotInitializeWhenItemsExist(): void {
$pool = $this->createMockPool();
$keyListItem = $this->createMockItem('__key_list', [], true);
$changeListItem = $this->createMockItem('__chg_list', [], true);
$pool->method('getItems')
->with(['__key_list', '__chg_list'])
->willReturn([$keyListItem, $changeListItem]);
$pool->expects(self::never())->method('saveDeferred');
$pool->expects(self::never())->method('commit');
new MonitorCacheKeys($pool);
}
public function testGetKeys(): void {
$pool = $this->createMockPool();
$keyListItem = $this->createMockItem('__key_list', ['key1' => true, 'key2' => true], true);
$changeListItem = $this->createMockItem('__chg_list', [], true);
$pool->method('getItems')
->with(['__key_list', '__chg_list'])
->willReturn([$keyListItem, $changeListItem]);
$pool->method('getItem')
->with('__key_list')
->willReturn($keyListItem);
$monitor = new MonitorCacheKeys($pool);
self::assertSame(['key1', 'key2'], $monitor->getKeys());
}
public function testGetChanges(): void {
$pool = $this->createMockPool();
$keyListItem = $this->createMockItem('__key_list', [], true);
$changeListItem = $this->createMockItem('__chg_list', ['key1' => MonitorCacheKeys::UPDATED], true);
$pool->method('getItems')
->with(['__key_list', '__chg_list'])
->willReturn([$keyListItem, $changeListItem]);
$pool->method('getItem')
->with('__chg_list')
->willReturn($changeListItem);
$monitor = new MonitorCacheKeys($pool);
self::assertSame(['key1' => MonitorCacheKeys::UPDATED], $monitor->getChanges());
}
public function testMarkClean(): void {
$pool = $this->createMockPool();
$keyListItem = $this->createMockItem('__key_list', [], true);
$changeListItem = $this->createMockItem('__chg_list', ['key1' => MonitorCacheKeys::UPDATED], true);
$pool->method('getItems')
->with(['__key_list', '__chg_list'])
->willReturn([$keyListItem, $changeListItem]);
$pool->method('getItem')
->with('__chg_list')
->willReturn($changeListItem);
$pool->expects(self::once())
->method('save')
->with(self::callback(function ($item) {
return $item->getKey() === '__chg_list' && $item->get() === [];
}))
->willReturn(true);
$monitor = new MonitorCacheKeys($pool);
$monitor->markClean();
}
public function testGetItemDelegates(): void {
$pool = $this->createMockPool();
$keyListItem = $this->createMockItem('__key_list', [], true);
$changeListItem = $this->createMockItem('__chg_list', [], true);
$requestedItem = $this->createMockItem('mykey', 'value', true);
$pool->method('getItems')
->with(['__key_list', '__chg_list'])
->willReturn([$keyListItem, $changeListItem]);
$pool->method('getItem')
->with('mykey')
->willReturn($requestedItem);
$monitor = new MonitorCacheKeys($pool);
self::assertSame($requestedItem, $monitor->getItem('mykey'));
}
public function testGetItemsDelegates(): void {
$pool = $this->createMockPool();
$keyListItem = $this->createMockItem('__key_list', [], true);
$changeListItem = $this->createMockItem('__chg_list', [], true);
$pool->method('getItems')
->willReturnCallback(function ($keys) {
return array_map(fn($k) => $this->createMockItem($k, null, true), $keys);
});
$monitor = new MonitorCacheKeys($pool);
$items = iterator_to_array($monitor->getItems(['a', 'b']));
self::assertCount(2, $items);
}
public function testHasItemDelegates(): void {
$pool = $this->createMockPool();
$keyListItem = $this->createMockItem('__key_list', [], true);
$changeListItem = $this->createMockItem('__chg_list', [], true);
$pool->method('getItems')
->with(['__key_list', '__chg_list'])
->willReturn([$keyListItem, $changeListItem]);
$pool->method('hasItem')
->with('mykey')
->willReturn(true);
$monitor = new MonitorCacheKeys($pool);
self::assertTrue($monitor->hasItem('mykey'));
}
public function testClearWhenNotEmpty(): void {
$pool = $this->createMockPool();
$keyListItem = $this->createMockItem('__key_list', ['key1' => true], true);
$changeListItem = $this->createMockItem('__chg_list', [], true);
$pool->method('getItems')
->with(['__key_list', '__chg_list'])
->willReturn([$keyListItem, $changeListItem]);
$pool->method('getItem')
->with('__key_list')
->willReturn($keyListItem);
$pool->expects(self::once())->method('clear')->willReturn(true);
$pool->expects(self::exactly(2))->method('saveDeferred')->willReturn(true);
$pool->expects(self::once())->method('commit')->willReturn(true);
$monitor = new MonitorCacheKeys($pool);
self::assertTrue($monitor->clear());
}
public function testClearWhenEmpty(): void {
$pool = $this->createMockPool();
$keyListItem = $this->createMockItem('__key_list', [], true);
$changeListItem = $this->createMockItem('__chg_list', [], true);
$pool->method('getItems')
->with(['__key_list', '__chg_list'])
->willReturn([$keyListItem, $changeListItem]);
$pool->method('getItem')
->with('__key_list')
->willReturn($keyListItem);
$pool->expects(self::never())->method('clear');
$monitor = new MonitorCacheKeys($pool);
self::assertTrue($monitor->clear());
}
public function testDeleteItemUpdatesKeyList(): void {
$pool = $this->createMockPool();
$keyListItem = $this->createMockItem('__key_list', ['mykey' => true], true);
$changeListItem = $this->createMockItem('__chg_list', [], true);
$pool->method('getItems')
->with(['__key_list', '__chg_list'])
->willReturn([$keyListItem, $changeListItem]);
$pool->method('getItem')
->willReturnCallback(function ($key) use ($keyListItem, $changeListItem) {
return match ($key) {
'__key_list' => $keyListItem,
'__chg_list' => $changeListItem,
default => $this->createMockItem($key, null, false),
};
});
$pool->expects(self::once())->method('deleteItem')->with('mykey')->willReturn(true);
$pool->expects(self::exactly(2))->method('saveDeferred')->willReturn(true);
$pool->expects(self::once())->method('commit')->willReturn(true);
$monitor = new MonitorCacheKeys($pool);
self::assertTrue($monitor->deleteItem('mykey'));
}
public function testDeleteItemThrowsOnPrivateKey(): void {
$pool = $this->createMockPool();
$keyListItem = $this->createMockItem('__key_list', [], true);
$changeListItem = $this->createMockItem('__chg_list', [], true);
$pool->method('getItems')
->with(['__key_list', '__chg_list'])
->willReturn([$keyListItem, $changeListItem]);
$monitor = new MonitorCacheKeys($pool);
$this->expectException(OutOfBoundsException::class);
$monitor->deleteItem('__key_list');
}
public function testDeleteItemsUpdatesKeyList(): void {
$pool = $this->createMockPool();
$keyListItem = $this->createMockItem('__key_list', ['a' => true, 'b' => true], true);
$changeListItem = $this->createMockItem('__chg_list', [], true);
$pool->method('getItems')
->with(['__key_list', '__chg_list'])
->willReturn([$keyListItem, $changeListItem]);
$pool->method('getItem')
->with('__key_list')
->willReturn($keyListItem);
$pool->expects(self::once())->method('deleteItems')->with(['a', 'b'])->willReturn(true);
$pool->expects(self::once())->method('saveDeferred')->willReturn(true);
$pool->expects(self::once())->method('commit')->willReturn(true);
$monitor = new MonitorCacheKeys($pool);
self::assertTrue($monitor->deleteItems(['a', 'b']));
}
public function testDeleteItemsThrowsOnPrivateKey(): void {
$pool = $this->createMockPool();
$keyListItem = $this->createMockItem('__key_list', [], true);
$changeListItem = $this->createMockItem('__chg_list', [], true);
$pool->method('getItems')
->with(['__key_list', '__chg_list'])
->willReturn([$keyListItem, $changeListItem]);
$monitor = new MonitorCacheKeys($pool);
$this->expectException(OutOfBoundsException::class);
$monitor->deleteItems(['a', '__chg_list']);
}
public function testSaveUpdatesKeyList(): void {
$pool = $this->createMockPool();
$keyListItem = $this->createMockItem('__key_list', [], true);
$changeListItem = $this->createMockItem('__chg_list', [], true);
$newItem = $this->createMockItem('newkey', 'value', true);
$pool->method('getItems')
->with(['__key_list', '__chg_list'])
->willReturn([$keyListItem, $changeListItem]);
$pool->method('getItem')
->willReturnCallback(function ($key) use ($keyListItem, $changeListItem) {
return match ($key) {
'__key_list' => $keyListItem,
'__chg_list' => $changeListItem,
default => $this->createMockItem($key, null, false),
};
});
$pool->expects(self::once())->method('save')->with($newItem)->willReturn(true);
$pool->expects(self::exactly(2))->method('saveDeferred')->willReturn(true);
$pool->expects(self::once())->method('commit')->willReturn(true);
$monitor = new MonitorCacheKeys($pool);
self::assertTrue($monitor->save($newItem));
}
public function testSaveThrowsOnPrivateKey(): void {
$pool = $this->createMockPool();
$keyListItem = $this->createMockItem('__key_list', [], true);
$changeListItem = $this->createMockItem('__chg_list', [], true);
$badItem = $this->createMockItem('__key_list', 'value', true);
$pool->method('getItems')
->with(['__key_list', '__chg_list'])
->willReturn([$keyListItem, $changeListItem]);
$monitor = new MonitorCacheKeys($pool);
$this->expectException(OutOfBoundsException::class);
$monitor->save($badItem);
}
public function testSaveDeferredUpdatesKeyList(): void {
$pool = $this->createMockPool();
$keyListItem = $this->createMockItem('__key_list', [], true);
$changeListItem = $this->createMockItem('__chg_list', [], true);
$newItem = $this->createMockItem('newkey', 'value', true);
$pool->method('getItems')
->with(['__key_list', '__chg_list'])
->willReturn([$keyListItem, $changeListItem]);
$pool->method('getItem')
->willReturnCallback(function ($key) use ($keyListItem, $changeListItem) {
return match ($key) {
'__key_list' => $keyListItem,
'__chg_list' => $changeListItem,
default => $this->createMockItem($key, null, false),
};
});
$pool->expects(self::once())->method('saveDeferred')->with($newItem)->willReturn(true);
$pool->expects(self::exactly(2))->method('saveDeferred')->willReturn(true);
$pool->expects(self::once())->method('commit')->willReturn(true);
$monitor = new MonitorCacheKeys($pool);
self::assertTrue($monitor->saveDeferred($newItem));
}
public function testCommitDelegates(): void {
$pool = $this->createMockPool();
$keyListItem = $this->createMockItem('__key_list', [], true);
$changeListItem = $this->createMockItem('__chg_list', [], true);
$pool->method('getItems')
->with(['__key_list', '__chg_list'])
->willReturn([$keyListItem, $changeListItem]);
$pool->expects(self::once())->method('commit')->willReturn(true);
$monitor = new MonitorCacheKeys($pool);
self::assertTrue($monitor->commit());
}
}
+136
View File
@@ -0,0 +1,136 @@
<?php
declare(strict_types=1);
namespace App\Tests\Unit\Core;
use App\MonitorCacheKeys;
use App\PersistCache;
use PHPUnit\Framework\TestCase;
use Psr\Cache\CacheItemInterface;
use Psr\Cache\CacheItemPoolInterface;
final class PersistCacheTest extends TestCase {
private function createMockItem(string $key, mixed $value = null, bool $isHit = true): CacheItemInterface {
$item = $this->createMock(CacheItemInterface::class);
$item->method('getKey')->willReturn($key);
$item->method('get')->willReturn($value);
$item->method('isHit')->willReturn($isHit);
return $item;
}
private function createMockPool(): CacheItemPoolInterface {
return $this->createMock(CacheItemPoolInterface::class);
}
public function testBootWhenCacheIsEmpty(): void {
$sessionCache = $this->createMockPool();
$sessionStorage = $this->createMockPool();
$keyListItem = $this->createMockItem('__key_list', [], true);
$changeListItem = $this->createMockItem('__chg_list', [], true);
$sessionCache->method('getItems')
->with(['__key_list', '__chg_list'])
->willReturn([$keyListItem, $changeListItem]);
$sessionCache->method('getItem')
->with('__key_list')
->willReturn($keyListItem);
$storageKeyList = $this->createMockItem('__key_list', ['item1' => true], true);
$storageChangeList = $this->createMockItem('__chg_list', [], true);
$storageItem = $this->createMockItem('item1', 'value1', true);
$sessionStorage->method('getItems')
->with(['__key_list', '__chg_list'])
->willReturn([$storageKeyList, $storageChangeList]);
$sessionStorage->method('getItem')
->with('__key_list')
->willReturn($storageKeyList);
$sessionStorage->method('getItems')
->with(['item1'])
->willReturn(['item1' => $storageItem]);
$sessionCache->expects(self::once())->method('saveDeferred')->with($storageItem)->willReturn(true);
$sessionCache->expects(self::once())->method('commit')->willReturn(true);
$persistCache = new PersistCache($sessionCache, $sessionStorage);
$persistCache->boot();
}
public function testBootWhenCacheIsNotEmpty(): void {
$sessionCache = $this->createMockPool();
$sessionStorage = $this->createMockPool();
$keyListItem = $this->createMockItem('__key_list', ['item1' => true], true);
$changeListItem = $this->createMockItem('__chg_list', [], true);
$sessionCache->method('getItems')
->with(['__key_list', '__chg_list'])
->willReturn([$keyListItem, $changeListItem]);
$sessionCache->method('getItem')
->with('__key_list')
->willReturn($keyListItem);
$sessionStorage->expects(self::never())->method('getItems');
$sessionCache->expects(self::never())->method('saveDeferred');
$persistCache = new PersistCache($sessionCache, $sessionStorage);
$persistCache->boot();
}
public function testPersistWithChanges(): void {
$sessionCache = $this->createMockPool();
$sessionStorage = $this->createMockPool();
$keyListItem = $this->createMockItem('__key_list', ['item1' => true, 'item2' => true], true);
$changeListItem = $this->createMockItem('__chg_list', ['item1' => MonitorCacheKeys::UPDATED, 'item2' => MonitorCacheKeys::REMOVED], true);
$sessionCache->method('getItems')
->with(['__key_list', '__chg_list'])
->willReturn([$keyListItem, $changeListItem]);
$sessionCache->method('getItem')
->willReturnCallback(function ($key) use ($keyListItem, $changeListItem) {
return match ($key) {
'__key_list' => $keyListItem,
'__chg_list' => $changeListItem,
default => $this->createMockItem($key, 'value', true),
};
});
$item1 = $this->createMockItem('item1', 'value1', true);
$item2 = $this->createMockItem('item2', null, false);
$sessionCache->method('getItems')
->with(['item1', 'item2'])
->willReturn(['item1' => $item1, 'item2' => $item2]);
$sessionStorage->expects(self::once())->method('saveDeferred')->with($item1)->willReturn(true);
$sessionStorage->expects(self::once())->method('deleteItem')->with('item2')->willReturn(true);
$sessionStorage->expects(self::once())->method('commit')->willReturn(true);
$persistCache = new PersistCache($sessionCache, $sessionStorage);
$persistCache->persist();
}
public function testPersistWithNoChanges(): void {
$sessionCache = $this->createMockPool();
$sessionStorage = $this->createMockPool();
$keyListItem = $this->createMockItem('__key_list', [], true);
$changeListItem = $this->createMockItem('__chg_list', [], true);
$sessionCache->method('getItems')
->with(['__key_list', '__chg_list'])
->willReturn([$keyListItem, $changeListItem]);
$sessionCache->method('getItem')
->with('__chg_list')
->willReturn($changeListItem);
$sessionStorage->expects(self::never())->method('saveDeferred');
$sessionStorage->expects(self::never())->method('deleteItem');
$sessionStorage->expects(self::never())->method('commit');
$persistCache = new PersistCache($sessionCache, $sessionStorage);
$persistCache->persist();
}
}
+205
View File
@@ -0,0 +1,205 @@
<?php
declare(strict_types=1);
namespace App\Tests\Unit\Data;
use App\Data\Payload;
use App\Enum\Scope;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\InputBag;
final class PayloadTest extends TestCase {
public function testDecodeValidBase64Url(): void {
$data = json_encode([
'id' => 'testuser',
'token' => '123456',
'nonce' => 'abc123',
'json' => true,
'scope' => 'cookie',
]);
$base64url = rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
$payload = Payload::decode($base64url);
self::assertInstanceOf(Payload::class, $payload);
self::assertSame('testuser', $payload->id);
self::assertSame('123456', $payload->token);
self::assertSame('abc123', $payload->nonce);
self::assertTrue($payload->json);
self::assertSame(Scope::Cookie, $payload->scope);
}
public function testDecodeInvalidBase64UrlReturnsNull(): void {
self::assertNull(Payload::decode('!!!not-valid-base64!!!'));
}
public function testDecodeNonObjectJsonReturnsNull(): void {
$base64url = rtrim(strtr(base64_encode('"just a string"'), '+/', '-_'), '=');
self::assertNull(Payload::decode($base64url));
}
public function testDecodeEmptyStringReturnsNull(): void {
self::assertNull(Payload::decode(''));
}
public function testLoadWithValidInputBag(): void {
$input = new InputBag([
'username' => 'alice',
'nonce' => 'nonce123',
'totp' => '654321',
]);
$payload = Payload::load($input);
self::assertInstanceOf(Payload::class, $payload);
self::assertSame('alice', $payload->id);
self::assertSame('nonce123', $payload->nonce);
self::assertSame('654321', $payload->token);
self::assertFalse($payload->json);
self::assertSame(Scope::Cookie, $payload->scope);
}
public function testLoadMissingUsernameReturnsNull(): void {
$input = new InputBag([
'nonce' => 'nonce123',
'totp' => '654321',
]);
self::assertNull(Payload::load($input));
}
public function testLoadMissingNonceReturnsNull(): void {
$input = new InputBag([
'username' => 'alice',
'totp' => '654321',
]);
self::assertNull(Payload::load($input));
}
public function testLoadMissingTotpReturnsNull(): void {
$input = new InputBag([
'username' => 'alice',
'nonce' => 'nonce123',
]);
self::assertNull(Payload::load($input));
}
public function testLoadEmptyInputBagReturnsNull(): void {
$input = new InputBag([]);
self::assertNull(Payload::load($input));
}
public function testCreateWithValidData(): void {
$data = (object)[
'id' => 'user1',
'token' => 'tok1',
'nonce' => 'non1',
'json' => false,
'scope' => 'ip',
];
$payload = Payload::create($data);
self::assertInstanceOf(Payload::class, $payload);
self::assertSame('user1', $payload->id);
self::assertSame('tok1', $payload->token);
self::assertSame('non1', $payload->nonce);
self::assertFalse($payload->json);
self::assertSame(Scope::Ip, $payload->scope);
}
public function testCreateWithDefaultScope(): void {
$data = (object)[
'id' => 'user1',
'token' => 'tok1',
'nonce' => 'non1',
];
$payload = Payload::create($data);
self::assertSame(Scope::Cookie, $payload->scope);
}
public function testCreateWithNoneScopeSetsJsonFalse(): void {
$data = (object)[
'id' => 'user1',
'token' => 'tok1',
'nonce' => 'non1',
'json' => true,
'scope' => 'none',
];
$payload = Payload::create($data);
self::assertSame(Scope::None, $payload->scope);
self::assertFalse($payload->json);
}
public function testCreateWithEmptyIdReturnsNull(): void {
$data = (object)[
'id' => '',
'token' => 'tok1',
'nonce' => 'non1',
];
self::assertNull(Payload::create($data));
}
public function testCreateWithWhitespaceIdReturnsNull(): void {
$data = (object)[
'id' => ' ',
'token' => 'tok1',
'nonce' => 'non1',
];
self::assertNull(Payload::create($data));
}
public function testCreateWithEmptyTokenReturnsNull(): void {
$data = (object)[
'id' => 'user1',
'token' => '',
'nonce' => 'non1',
];
self::assertNull(Payload::create($data));
}
public function testCreateWithEmptyNonceReturnsNull(): void {
$data = (object)[
'id' => 'user1',
'token' => 'tok1',
'nonce' => '',
];
self::assertNull(Payload::create($data));
}
public function testCreateTrimsAndTruncatesFields(): void {
$long = str_repeat('a', 200);
$data = (object)[
'id' => ' ' . $long . ' ',
'token' => ' ' . $long . ' ',
'nonce' => ' ' . $long . ' ',
];
$payload = Payload::create($data);
self::assertSame(mb_substr($long, 0, 128), $payload->id);
self::assertSame(mb_substr($long, 0, 128), $payload->token);
self::assertSame(mb_substr($long, 0, 128), $payload->nonce);
}
public function testToString(): void {
$payload = new Payload();
$payload->id = 'u';
$payload->token = 't';
$payload->nonce = 'n';
$payload->json = true;
$payload->scope = Scope::Cookie;
$json = $payload->toString();
$decoded = json_decode($json, true);
self::assertSame('u', $decoded['id']);
self::assertSame('t', $decoded['token']);
self::assertSame('n', $decoded['nonce']);
self::assertTrue($decoded['json']);
self::assertSame('cookie', $decoded['scope']);
}
}
+27
View File
@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace App\Tests\Unit\Enum;
use App\Enum\Scope;
use PHPUnit\Framework\TestCase;
final class ScopeTest extends TestCase {
public function testCases(): void {
self::assertSame('cookie', Scope::Cookie->value);
self::assertSame('ip', Scope::Ip->value);
self::assertSame('none', Scope::None->value);
}
public function testTryFromValid(): void {
self::assertSame(Scope::Cookie, Scope::tryFrom('cookie'));
self::assertSame(Scope::Ip, Scope::tryFrom('ip'));
self::assertSame(Scope::None, Scope::tryFrom('none'));
}
public function testTryFromInvalidReturnsNull(): void {
self::assertNull(Scope::tryFrom('invalid'));
self::assertNull(Scope::tryFrom(''));
self::assertNull(Scope::tryFrom('COOKIE'));
}
}
+118
View File
@@ -0,0 +1,118 @@
<?php
declare(strict_types=1);
namespace App\Tests\Unit\Listener;
use App\Listener\AcceptListener;
use App\Service\DomainManager;
use PHPUnit\Framework\TestCase;
use Psr\Cache\CacheItemInterface;
use Psr\Cache\CacheItemPoolInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\HttpKernelInterface;
final class AcceptListenerTest extends TestCase {
private function createEvent(Request $request): RequestEvent {
$kernel = $this->createMock(HttpKernelInterface::class);
return new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST);
}
private function createMockItem(string $key, mixed $value = null, bool $isHit = true): CacheItemInterface {
$item = $this->createMock(CacheItemInterface::class);
$item->method('getKey')->willReturn($key);
$item->method('get')->willReturn($value);
$item->method('isHit')->willReturn($isHit);
return $item;
}
public function testOnKernelRequestWithValidCookie(): void {
$cache = $this->createMock(CacheItemPoolInterface::class);
$domainManager = new DomainManager(false, '');
$listener = new AcceptListener($cache, $domainManager);
$listener->setLogger($this->createMock(LoggerInterface::class));
$cacheItem = $this->createMockItem('cookie_abc123', 'user1', true);
$cache->method('hasItem')->with('cookie_abc123')->willReturn(true);
$cache->method('getItem')->with('cookie_abc123')->willReturn($cacheItem);
$request = Request::create('https://example.com/');
$request->cookies->set('__Host-Http-Preauth', 'abc123');
$event = $this->createEvent($request);
$listener->onKernelRequest($event);
$response = $event->getResponse();
self::assertNotNull($response);
self::assertSame(200, $response->getStatusCode());
self::assertSame('hi user1', $response->getContent());
self::assertSame('user1', $response->headers->get('Remote-User'));
}
public function testOnKernelRequestWithNoCookie(): void {
$cache = $this->createMock(CacheItemPoolInterface::class);
$domainManager = new DomainManager(false, '');
$listener = new AcceptListener($cache, $domainManager);
$listener->setLogger($this->createMock(LoggerInterface::class));
$request = Request::create('https://example.com/');
$event = $this->createEvent($request);
$listener->onKernelRequest($event);
self::assertNull($event->getResponse());
}
public function testOnKernelRequestWithInvalidCookie(): void {
$cache = $this->createMock(CacheItemPoolInterface::class);
$domainManager = new DomainManager(false, '');
$listener = new AcceptListener($cache, $domainManager);
$listener->setLogger($this->createMock(LoggerInterface::class));
$cache->method('hasItem')->with('cookie_badcookie')->willReturn(false);
$request = Request::create('https://example.com/');
$request->cookies->set('__Host-Http-Preauth', 'badcookie');
$event = $this->createEvent($request);
$listener->onKernelRequest($event);
self::assertNull($event->getResponse());
}
public function testOnKernelRequestWithAuthBaseUsesAuthCookieName(): void {
$cache = $this->createMock(CacheItemPoolInterface::class);
$domainManager = new DomainManager(true, 'auth.example.com');
$listener = new AcceptListener($cache, $domainManager);
$listener->setLogger($this->createMock(LoggerInterface::class));
$cacheItem = $this->createMockItem('cookie_xyz789', 'user2', true);
$cache->method('hasItem')->with('cookie_xyz789')->willReturn(true);
$cache->method('getItem')->with('cookie_xyz789')->willReturn($cacheItem);
$request = Request::create('https://example.com/');
$request->cookies->set('__Http-Domain-Preauth', 'xyz789');
$event = $this->createEvent($request);
$listener->onKernelRequest($event);
$response = $event->getResponse();
self::assertNotNull($response);
self::assertSame('hi user2', $response->getContent());
}
public function testOnKernelRequestWithEmptyCookieValue(): void {
$cache = $this->createMock(CacheItemPoolInterface::class);
$domainManager = new DomainManager(false, '');
$listener = new AcceptListener($cache, $domainManager);
$listener->setLogger($this->createMock(LoggerInterface::class));
$request = Request::create('https://example.com/');
$request->cookies->set('__Host-Http-Preauth', '');
$event = $this->createEvent($request);
$listener->onKernelRequest($event);
self::assertNull($event->getResponse());
}
}
+122
View File
@@ -0,0 +1,122 @@
<?php
declare(strict_types=1);
namespace App\Tests\Unit\Listener;
use App\ConfigBag;
use App\Listener\AllowListener;
use App\Utilities;
use OTPHP\TOTP;
use PHPUnit\Framework\TestCase;
use Psr\Cache\CacheItemInterface;
use Psr\Cache\CacheItemPoolInterface;
use Psr\Clock\ClockInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\HttpKernelInterface;
final class AllowListenerTest extends TestCase {
private function createEvent(Request $request): RequestEvent {
$kernel = $this->createMock(HttpKernelInterface::class);
return new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST);
}
private function createMockItem(string $key, mixed $value = null, bool $isHit = true): CacheItemInterface {
$item = $this->createMock(CacheItemInterface::class);
$item->method('getKey')->willReturn($key);
$item->method('get')->willReturn($value);
$item->method('isHit')->willReturn($isHit);
return $item;
}
private function createConfigBag(int $ipTtl = 1800): ConfigBag {
$clock = $this->createMock(ClockInterface::class);
$cache = $this->createMock(CacheItemPoolInterface::class);
$utilities = new Utilities($clock, $cache);
$totp = TOTP::generate($clock);
$totp->setLabel('Test');
return new ConfigBag(
$utilities,
$clock,
3600,
$totp->getProvisioningUri(),
$ipTtl,
false,
'Error',
'Teapot',
'Too Many'
);
}
public function testOnKernelRequestWithValidIp(): void {
$cache = $this->createMock(CacheItemPoolInterface::class);
$config = $this->createConfigBag(1800);
$listener = new AllowListener($cache, $config);
$listener->setLogger($this->createMock(LoggerInterface::class));
$cacheItem = $this->createMockItem('ip_192.168.1.1', 'user1', true);
$cache->method('hasItem')->with('ip_192.168.1.1')->willReturn(true);
$cache->method('getItem')->with('ip_192.168.1.1')->willReturn($cacheItem);
$request = Request::create('https://example.com/');
$request->server->set('REMOTE_ADDR', '192.168.1.1');
$event = $this->createEvent($request);
$listener->onKernelRequest($event);
$response = $event->getResponse();
self::assertNotNull($response);
self::assertSame(200, $response->getStatusCode());
self::assertSame('hi user1', $response->getContent());
self::assertSame('user1', $response->headers->get('Remote-User'));
}
public function testOnKernelRequestWithIpTtlZero(): void {
$cache = $this->createMock(CacheItemPoolInterface::class);
$config = $this->createConfigBag(0);
$listener = new AllowListener($cache, $config);
$listener->setLogger($this->createMock(LoggerInterface::class));
$request = Request::create('https://example.com/');
$request->server->set('REMOTE_ADDR', '192.168.1.1');
$event = $this->createEvent($request);
$listener->onKernelRequest($event);
self::assertNull($event->getResponse());
}
public function testOnKernelRequestWithIpTtlNull(): void {
$cache = $this->createMock(CacheItemPoolInterface::class);
$config = $this->createConfigBag(0);
$listener = new AllowListener($cache, $config);
$listener->setLogger($this->createMock(LoggerInterface::class));
$request = Request::create('https://example.com/');
$request->server->set('REMOTE_ADDR', '192.168.1.1');
$event = $this->createEvent($request);
$listener->onKernelRequest($event);
self::assertNull($event->getResponse());
}
public function testOnKernelRequestWithUnknownIp(): void {
$cache = $this->createMock(CacheItemPoolInterface::class);
$config = $this->createConfigBag(1800);
$listener = new AllowListener($cache, $config);
$listener->setLogger($this->createMock(LoggerInterface::class));
$cache->method('hasItem')->with('ip_192.168.1.1')->willReturn(false);
$request = Request::create('https://example.com/');
$request->server->set('REMOTE_ADDR', '192.168.1.1');
$event = $this->createEvent($request);
$listener->onKernelRequest($event);
self::assertNull($event->getResponse());
}
}
@@ -0,0 +1,169 @@
<?php
declare(strict_types=1);
namespace App\Tests\Unit\Listener;
use App\ConfigBag;
use App\Listener\InterceptListener;
use App\Service\DomainManager;
use App\Utilities;
use OTPHP\TOTP;
use PHPUnit\Framework\TestCase;
use Psr\Cache\CacheItemInterface;
use Psr\Cache\CacheItemPoolInterface;
use Psr\Clock\ClockInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Twig\Environment;
final class InterceptListenerTest extends TestCase {
private function createEvent(Request $request): RequestEvent {
$kernel = $this->createMock(HttpKernelInterface::class);
return new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST);
}
private function createConfigBag(): ConfigBag {
$clock = $this->createMock(ClockInterface::class);
$cache = $this->createMock(CacheItemPoolInterface::class);
$utilities = new Utilities($clock, $cache);
$totp = TOTP::generate($clock);
$totp->setLabel('Test');
return new ConfigBag(
$utilities,
$clock,
3600,
$totp->getProvisioningUri(),
1800,
false,
'Error',
'Teapot',
'Too Many'
);
}
private function createTwig(): Environment {
$twig = $this->createMock(Environment::class);
$twig->method('render')->with('login.html.twig', self::anything())->willReturn('<html>login</html>');
return $twig;
}
private function createListener(
?DomainManager $dm = null,
?Environment $twig = null,
?CacheItemPoolInterface $nonceCache = null
): InterceptListener {
$config = $this->createConfigBag();
$domainManager = $dm ?? new DomainManager(false, '');
$twig = $twig ?? $this->createTwig();
$listener = new InterceptListener($config, $domainManager, $twig);
$listener->setLogger($this->createMock(LoggerInterface::class));
if ($nonceCache) {
$listener->setNonceCache($nonceCache);
} else {
$cache = $this->createMock(CacheItemPoolInterface::class);
$item = $this->createMock(CacheItemInterface::class);
$item->method('isHit')->willReturn(false);
$item->method('set')->willReturnSelf();
$item->method('expiresAfter')->willReturnSelf();
$cache->method('getItem')->willReturn($item);
$cache->method('save')->willReturn(true);
$listener->setNonceCache($cache);
}
return $listener;
}
public function testOnKernelRequestRedirectsToAuthSubdomain(): void {
$dm = new DomainManager(true, 'auth.example.com');
$listener = $this->createListener($dm);
$request = Request::create('https://www.example.com/protected');
$event = $this->createEvent($request);
$listener->onKernelRequest($event);
$response = $event->getResponse();
self::assertNotNull($response);
self::assertSame(303, $response->getStatusCode());
self::assertStringContainsString('auth.example.com', $response->headers->get('Location'));
self::assertStringContainsString('return=', $response->headers->get('Location'));
}
public function testOnKernelRequestPresentsLoginPageOnAuthSubdomain(): void {
$dm = new DomainManager(true, 'auth.example.com');
$listener = $this->createListener($dm);
$request = Request::create('https://auth.example.com/');
$event = $this->createEvent($request);
$listener->onKernelRequest($event);
$response = $event->getResponse();
self::assertNotNull($response);
self::assertSame(401, $response->getStatusCode());
self::assertSame('<html>login</html>', $response->getContent());
}
public function testOnKernelRequestPresentsLoginPageWithoutAuthSubdomain(): void {
$dm = new DomainManager(false, '');
$listener = $this->createListener($dm);
$request = Request::create('https://example.com/');
$event = $this->createEvent($request);
$listener->onKernelRequest($event);
$response = $event->getResponse();
self::assertNotNull($response);
self::assertSame(401, $response->getStatusCode());
}
public function testOnKernelRequestPrunesInvalidCookie(): void {
$dm = new DomainManager(false, '');
$listener = $this->createListener($dm);
$request = Request::create('https://example.com/');
$request->cookies->set('__Host-Http-Preauth', 'invalid');
$event = $this->createEvent($request);
$listener->onKernelRequest($event);
$response = $event->getResponse();
self::assertNotNull($response);
$cookies = $response->headers->getCookies();
self::assertCount(1, $cookies);
self::assertSame('__Host-Http-Preauth', $cookies[0]->getName());
self::assertNull($cookies[0]->getValue());
}
public function testOnKernelRequestDoesNotPruneCookieWhenNotPresent(): void {
$dm = new DomainManager(false, '');
$listener = $this->createListener($dm);
$request = Request::create('https://example.com/');
$event = $this->createEvent($request);
$listener->onKernelRequest($event);
$response = $event->getResponse();
self::assertNotNull($response);
self::assertCount(0, $response->headers->getCookies());
}
public function testOnKernelRequestWithAuthBaseUsesAuthCookieName(): void {
$dm = new DomainManager(true, 'auth.example.com');
$listener = $this->createListener($dm);
$request = Request::create('https://auth.example.com/');
$request->cookies->set('__Http-Domain-Preauth', 'invalid');
$event = $this->createEvent($request);
$listener->onKernelRequest($event);
$response = $event->getResponse();
self::assertNotNull($response);
$cookies = $response->headers->getCookies();
self::assertCount(1, $cookies);
self::assertSame('__Http-Domain-Preauth', $cookies[0]->getName());
}
}
+253
View File
@@ -0,0 +1,253 @@
<?php
declare(strict_types=1);
namespace App\Tests\Unit\Listener;
use App\ConfigBag;
use App\Data\Payload;
use App\Listener\LoginListener;
use App\Service\DomainManager;
use App\Service\LoginManager;
use App\Utilities;
use OTPHP\TOTP;
use PHPUnit\Framework\TestCase;
use Psr\Cache\CacheItemInterface;
use Psr\Cache\CacheItemPoolInterface;
use Psr\Clock\ClockInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\RateLimiter\RateLimit;
use Symfony\Component\RateLimiter\RateLimiterFactoryInterface;
use Symfony\Component\RateLimiter\LimiterInterface;
use Twig\Environment;
final class LoginListenerTest extends TestCase {
private function createEvent(Request $request): RequestEvent {
$kernel = $this->createMock(HttpKernelInterface::class);
return new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST);
}
private function createConfigBag(bool $teapot = false): ConfigBag {
$clock = $this->createMock(ClockInterface::class);
$cache = $this->createMock(CacheItemPoolInterface::class);
$utilities = new Utilities($clock, $cache);
$totp = TOTP::generate($clock);
$totp->setLabel('Test');
return new ConfigBag(
$utilities,
$clock,
3600,
$totp->getProvisioningUri(),
1800,
$teapot,
'Error!',
'Teapot!',
'Too Many!'
);
}
private function createLimiter(int $remainingTokens): LimiterInterface {
$limit = $this->createMock(RateLimit::class);
$limit->method('getRemainingTokens')->willReturn($remainingTokens);
$limiter = $this->createMock(LimiterInterface::class);
$limiter->method('consume')->with(1)->willReturn($limit);
return $limiter;
}
private function createListener(
?LoginManager $loginManager = null,
?DomainManager $domainManager = null,
?RateLimiterFactoryInterface $rateLimiter = null,
bool $teapot = false
): LoginListener {
$twig = $this->createMock(Environment::class);
$twig->method('render')->with('login.html.twig', self::anything())->willReturn('<html>login</html>');
$rateLimiter = $rateLimiter ?? $this->createMock(RateLimiterFactoryInterface::class);
$rateLimiter->method('create')->willReturn($this->createLimiter(5));
$dm = $domainManager ?? new DomainManager(false, '');
$lm = $loginManager ?? $this->createMock(LoginManager::class);
$config = $this->createConfigBag($teapot);
$listener = new LoginListener($twig, $rateLimiter, $dm, $lm, $config);
$listener->setLogger($this->createMock(LoggerInterface::class));
$cache = $this->createMock(CacheItemPoolInterface::class);
$item = $this->createMock(CacheItemInterface::class);
$item->method('isHit')->willReturn(false);
$item->method('set')->willReturnSelf();
$item->method('expiresAfter')->willReturnSelf();
$cache->method('getItem')->willReturn($item);
$cache->method('save')->willReturn(true);
$listener->setNonceCache($cache);
return $listener;
}
public function testOnKernelRequestWithNoLoginAttempt(): void {
$listener = $this->createListener();
$request = Request::create('https://example.com/');
$event = $this->createEvent($request);
$listener->onKernelRequest($event);
self::assertNull($event->getResponse());
}
public function testOnKernelRequestWithHeaderLoginSuccess(): void {
$loginManager = $this->createMock(LoginManager::class);
$loginManager->method('checkToken')->willReturnCallback(function (Payload $payload) {
return new \Symfony\Component\HttpFoundation\Response('success');
});
$listener = $this->createListener($loginManager);
$request = Request::create('https://example.com/');
$request->headers->set('X-Preauth', base64_encode(json_encode([
'id' => 'user1', 'token' => '123456', 'nonce' => 'abc', 'json' => true, 'scope' => 'cookie'
])));
$event = $this->createEvent($request);
$listener->onKernelRequest($event);
$response = $event->getResponse();
self::assertNotNull($response);
self::assertSame('success', $response->getContent());
}
public function testOnKernelRequestWithPostLoginSuccess(): void {
$loginManager = $this->createMock(LoginManager::class);
$loginManager->method('checkToken')->willReturn(new \Symfony\Component\HttpFoundation\Response('success'));
$dm = new DomainManager(true, 'auth.example.com');
$listener = $this->createListener($loginManager, $dm);
$request = Request::create('https://auth.example.com/', 'POST', [
'username' => 'user1',
'nonce' => 'abc',
'totp' => '123456',
]);
$event = $this->createEvent($request);
$listener->onKernelRequest($event);
$response = $event->getResponse();
self::assertNotNull($response);
self::assertSame('success', $response->getContent());
}
public function testOnKernelRequestWithFailedLoginReturnsUnauthorized(): void {
$loginManager = $this->createMock(LoginManager::class);
$loginManager->method('checkToken')->willReturn(null);
$listener = $this->createListener($loginManager);
$request = Request::create('https://example.com/');
$request->headers->set('X-Preauth', base64_encode(json_encode([
'id' => 'user1', 'token' => 'bad', 'nonce' => 'abc', 'json' => true, 'scope' => 'cookie'
])));
$event = $this->createEvent($request);
$listener->onKernelRequest($event);
$response = $event->getResponse();
self::assertNotNull($response);
self::assertSame(401, $response->getStatusCode());
}
public function testOnKernelRequestWithRateLimitReached(): void {
$loginManager = $this->createMock(LoginManager::class);
$loginManager->method('checkToken')->willReturn(null);
$rateLimiter = $this->createMock(RateLimiterFactoryInterface::class);
$limit = $this->createMock(RateLimit::class);
$limit->method('getRemainingTokens')->willReturn(0);
$limiter = $this->createMock(LimiterInterface::class);
$limiter->method('consume')->with(1)->willReturn($limit);
$rateLimiter->method('create')->willReturn($limiter);
$listener = $this->createListener($loginManager, null, $rateLimiter);
$request = Request::create('https://example.com/');
$request->headers->set('X-Preauth', base64_encode(json_encode([
'id' => 'user1', 'token' => 'bad', 'nonce' => 'abc', 'json' => true, 'scope' => 'cookie'
])));
$event = $this->createEvent($request);
$listener->onKernelRequest($event);
$response = $event->getResponse();
self::assertNotNull($response);
self::assertSame(429, $response->getStatusCode());
}
public function testOnKernelRequestWithRateLimitReachedAndTeapot(): void {
$loginManager = $this->createMock(LoginManager::class);
$loginManager->method('checkToken')->willReturn(null);
$rateLimiter = $this->createMock(RateLimiterFactoryInterface::class);
$limit = $this->createMock(RateLimit::class);
$limit->method('getRemainingTokens')->willReturn(0);
$limiter = $this->createMock(LimiterInterface::class);
$limiter->method('consume')->with(1)->willReturn($limit);
$rateLimiter->method('create')->willReturn($limiter);
$listener = $this->createListener($loginManager, null, $rateLimiter, true);
$request = Request::create('https://example.com/');
$request->headers->set('X-Preauth', base64_encode(json_encode([
'id' => 'user1', 'token' => 'bad', 'nonce' => 'abc', 'json' => true, 'scope' => 'cookie'
])));
$event = $this->createEvent($request);
$listener->onKernelRequest($event);
$response = $event->getResponse();
self::assertNotNull($response);
self::assertSame(418, $response->getStatusCode());
}
public function testOnKernelRequestWithJsonResponse(): void {
$loginManager = $this->createMock(LoginManager::class);
$loginManager->method('checkToken')->willReturn(null);
$listener = $this->createListener($loginManager);
$request = Request::create('https://example.com/');
$request->headers->set('X-Preauth', base64_encode(json_encode([
'id' => 'user1', 'token' => 'bad', 'nonce' => 'abc', 'json' => true, 'scope' => 'cookie'
])));
$event = $this->createEvent($request);
$listener->onKernelRequest($event);
$response = $event->getResponse();
self::assertNotNull($response);
self::assertSame('application/json', $response->headers->get('Content-Type'));
}
public function testOnKernelRequestWithHtmlResponse(): void {
$loginManager = $this->createMock(LoginManager::class);
$loginManager->method('checkToken')->willReturn(null);
$listener = $this->createListener($loginManager);
$request = Request::create('https://auth.example.com/', 'POST', [
'username' => 'user1',
'nonce' => 'abc',
'totp' => 'bad',
]);
$event = $this->createEvent($request);
$listener->onKernelRequest($event);
$response = $event->getResponse();
self::assertNotNull($response);
self::assertSame('text/html', $response->headers->get('Content-Type'));
}
}
+110
View File
@@ -0,0 +1,110 @@
<?php
declare(strict_types=1);
namespace App\Tests\Unit\Listener;
use App\ConfigBag;
use App\Listener\RejectListener;
use OTPHP\TOTP;
use PHPUnit\Framework\TestCase;
use Psr\Clock\ClockInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\RateLimiter\RateLimit;
use Symfony\Component\RateLimiter\RateLimiterFactoryInterface;
use Symfony\Component\RateLimiter\LimiterInterface;
use Twig\Environment;
final class RejectListenerTest extends TestCase {
private function createEvent(Request $request): RequestEvent {
$kernel = $this->createMock(HttpKernelInterface::class);
return new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST);
}
private function createConfigBag(bool $teapot = false): ConfigBag {
$clock = $this->createMock(ClockInterface::class);
$utilities = $this->createMock(\App\Utilities::class);
$totp = TOTP::generate($clock);
$totp->setLabel('Test');
return new ConfigBag(
$utilities,
$clock,
3600,
$totp->getProvisioningUri(),
1800,
$teapot,
'Error',
'Teapot!',
'Too Many'
);
}
private function createLimiter(int $remainingTokens): LimiterInterface {
$limit = $this->createMock(RateLimit::class);
$limit->method('getRemainingTokens')->willReturn($remainingTokens);
$limiter = $this->createMock(LimiterInterface::class);
$limiter->method('consume')->with(0)->willReturn($limit);
return $limiter;
}
public function testOnKernelRequestWhenNotBlocked(): void {
$twig = $this->createMock(Environment::class);
$config = $this->createConfigBag();
$rateLimiter = $this->createMock(RateLimiterFactoryInterface::class);
$rateLimiter->method('create')->willReturn($this->createLimiter(5));
$listener = new RejectListener($config, $twig, $rateLimiter);
$listener->setLogger($this->createMock(LoggerInterface::class));
$request = Request::create('https://example.com/');
$event = $this->createEvent($request);
$listener->onKernelRequest($event);
self::assertNull($event->getResponse());
}
public function testOnKernelRequestWhenBlocked(): void {
$twig = $this->createMock(Environment::class);
$twig->method('render')->with('error.html.twig')->willReturn('<html>blocked</html>');
$config = $this->createConfigBag();
$rateLimiter = $this->createMock(RateLimiterFactoryInterface::class);
$rateLimiter->method('create')->willReturn($this->createLimiter(0));
$listener = new RejectListener($config, $twig, $rateLimiter);
$listener->setLogger($this->createMock(LoggerInterface::class));
$request = Request::create('https://example.com/');
$event = $this->createEvent($request);
$listener->onKernelRequest($event);
$response = $event->getResponse();
self::assertNotNull($response);
self::assertSame(429, $response->getStatusCode());
self::assertSame('<html>blocked</html>', $response->getContent());
}
public function testOnKernelRequestWhenBlockedWithTeapot(): void {
$twig = $this->createMock(Environment::class);
$twig->method('render')->with('error.html.twig')->willReturn('<html>teapot</html>');
$config = $this->createConfigBag(true);
$rateLimiter = $this->createMock(RateLimiterFactoryInterface::class);
$rateLimiter->method('create')->willReturn($this->createLimiter(0));
$listener = new RejectListener($config, $twig, $rateLimiter);
$listener->setLogger($this->createMock(LoggerInterface::class));
$request = Request::create('https://example.com/');
$event = $this->createEvent($request);
$listener->onKernelRequest($event);
$response = $event->getResponse();
self::assertNotNull($response);
self::assertSame(418, $response->getStatusCode());
}
}
@@ -0,0 +1,253 @@
<?php
declare(strict_types=1);
namespace App\Tests\Unit\Service;
use App\ConfigBag;
use App\Service\BackupCodeManager;
use OTPHP\TOTP;
use PHPUnit\Framework\TestCase;
use Psr\Cache\CacheItemInterface;
use Psr\Cache\CacheItemPoolInterface;
use Psr\Clock\ClockInterface;
use Psr\Log\LoggerInterface;
final class BackupCodeManagerTest extends TestCase {
private function createMockPool(): CacheItemPoolInterface {
return $this->createMock(CacheItemPoolInterface::class);
}
private function createMockItem(string $key, mixed $value = null, bool $isHit = true): CacheItemInterface {
$item = $this->createMock(CacheItemInterface::class);
$item->method('getKey')->willReturn($key);
$item->method('get')->willReturn($value);
$item->method('isHit')->willReturn($isHit);
$item->method('set')->willReturnSelf();
$item->method('expiresAt')->willReturnSelf();
return $item;
}
private function createConfigBag(): ConfigBag {
$clock = $this->createMock(ClockInterface::class);
$utilities = $this->createMock(\App\Utilities::class);
$totp = TOTP::generate($clock);
$totp->setLabel('Test');
return new ConfigBag(
$utilities,
$clock,
3600,
$totp->getProvisioningUri(),
1800,
false,
'Error',
'Teapot',
'Too Many'
);
}
private function createManager(CacheItemPoolInterface $pool): BackupCodeManager {
$manager = new BackupCodeManager($pool);
$manager->setConfig($this->createConfigBag());
$manager->setLogger($this->createMock(LoggerInterface::class));
return $manager;
}
public function testGenerateCreatesCodes(): void {
$pool = $this->createMockPool();
$keyListItem = $this->createMockItem('__key_list', [], true);
$changeListItem = $this->createMockItem('__chg_list', [], true);
$pool->method('getItems')
->with(['__key_list', '__chg_list'])
->willReturn([$keyListItem, $changeListItem]);
$pool->method('getItem')
->willReturnCallback(function ($key) {
return $this->createMockItem($key, null, false);
});
$pool->expects(self::atLeastOnce())->method('saveDeferred')->willReturn(true);
$pool->expects(self::atLeastOnce())->method('commit')->willReturn(true);
$manager = $this->createManager($pool);
$codes = $manager->generate(5);
self::assertCount(5, $codes);
foreach ($codes as $code) {
self::assertMatchesRegularExpression('/^[a-z0-9]+$/', $code);
self::assertLessThanOrEqual(BackupCodeManager::MAX_LENGTH, strlen($code));
}
}
public function testGenerateCodesAreUnique(): void {
$pool = $this->createMockPool();
$keyListItem = $this->createMockItem('__key_list', [], true);
$changeListItem = $this->createMockItem('__chg_list', [], true);
$pool->method('getItems')
->with(['__key_list', '__chg_list'])
->willReturn([$keyListItem, $changeListItem]);
$pool->method('getItem')
->willReturnCallback(function ($key) {
return $this->createMockItem($key, null, false);
});
$pool->method('saveDeferred')->willReturn(true);
$pool->method('commit')->willReturn(true);
$manager = $this->createManager($pool);
$codes = $manager->generate(10);
self::assertSame($codes, array_unique($codes));
}
public function testVerifyAndConsumeWithValidCode(): void {
$pool = $this->createMockPool();
$keyListItem = $this->createMockItem('__key_list', [], true);
$changeListItem = $this->createMockItem('__chg_list', [], true);
$backupItem = $this->createMockItem('backup_abc123', true, true);
$pool->method('getItems')
->with(['__key_list', '__chg_list'])
->willReturn([$keyListItem, $changeListItem]);
$pool->method('getItem')
->willReturnCallback(function ($key) use ($backupItem) {
if (str_contains($key, 'backup_')) {
return $backupItem;
}
return $this->createMockItem($key, null, false);
});
$pool->expects(self::atLeastOnce())->method('save')->willReturn(true);
$manager = $this->createManager($pool);
self::assertTrue($manager->verifyAndConsume('abc123'));
}
public function testVerifyAndConsumeWithInvalidCode(): void {
$pool = $this->createMockPool();
$keyListItem = $this->createMockItem('__key_list', [], true);
$changeListItem = $this->createMockItem('__chg_list', [], true);
$backupItem = $this->createMockItem('backup_invalid', false, true);
$pool->method('getItems')
->with(['__key_list', '__chg_list'])
->willReturn([$keyListItem, $changeListItem]);
$pool->method('getItem')
->willReturnCallback(function ($key) use ($backupItem) {
if (str_contains($key, 'backup_')) {
return $backupItem;
}
return $this->createMockItem($key, null, false);
});
$manager = $this->createManager($pool);
self::assertFalse($manager->verifyAndConsume('invalid'));
}
public function testVerifyAndConsumeWithMissingCode(): void {
$pool = $this->createMockPool();
$keyListItem = $this->createMockItem('__key_list', [], true);
$changeListItem = $this->createMockItem('__chg_list', [], true);
$backupItem = $this->createMockItem('backup_missing', null, false);
$pool->method('getItems')
->with(['__key_list', '__chg_list'])
->willReturn([$keyListItem, $changeListItem]);
$pool->method('getItem')
->willReturnCallback(function ($key) use ($backupItem) {
if (str_contains($key, 'backup_')) {
return $backupItem;
}
return $this->createMockItem($key, null, false);
});
$manager = $this->createManager($pool);
self::assertFalse($manager->verifyAndConsume('missing'));
}
public function testVerifyAndConsumeIsCaseInsensitive(): void {
$pool = $this->createMockPool();
$keyListItem = $this->createMockItem('__key_list', [], true);
$changeListItem = $this->createMockItem('__chg_list', [], true);
$backupItem = $this->createMockItem('backup_abc123', true, true);
$pool->method('getItems')
->with(['__key_list', '__chg_list'])
->willReturn([$keyListItem, $changeListItem]);
$pool->method('getItem')
->willReturnCallback(function ($key) use ($backupItem) {
if (str_contains($key, 'backup_')) {
return $backupItem;
}
return $this->createMockItem($key, null, false);
});
$pool->expects(self::atLeastOnce())->method('save')->willReturn(true);
$manager = $this->createManager($pool);
self::assertTrue($manager->verifyAndConsume('ABC123'));
self::assertTrue($manager->verifyAndConsume('AbC123'));
}
public function testVerifyAndConsumeStripsInvalidChars(): void {
$pool = $this->createMockPool();
$keyListItem = $this->createMockItem('__key_list', [], true);
$changeListItem = $this->createMockItem('__chg_list', [], true);
$backupItem = $this->createMockItem('backup_abc123', true, true);
$pool->method('getItems')
->with(['__key_list', '__chg_list'])
->willReturn([$keyListItem, $changeListItem]);
$pool->method('getItem')
->willReturnCallback(function ($key) use ($backupItem) {
if (str_contains($key, 'backup_')) {
return $backupItem;
}
return $this->createMockItem($key, null, false);
});
$pool->expects(self::atLeastOnce())->method('save')->willReturn(true);
$manager = $this->createManager($pool);
self::assertTrue($manager->verifyAndConsume('abc-123'));
self::assertTrue($manager->verifyAndConsume('abc 123'));
self::assertTrue($manager->verifyAndConsume('abc!@#123'));
}
public function testExpireRemovesBackupCodes(): void {
$pool = $this->createMockPool();
$keyListItem = $this->createMockItem('__key_list', ['backup_a' => true, 'backup_b' => true, 'other' => true], true);
$changeListItem = $this->createMockItem('__chg_list', [], true);
$pool->method('getItems')
->with(['__key_list', '__chg_list'])
->willReturn([$keyListItem, $changeListItem]);
$pool->method('getItem')
->with('__key_list')
->willReturn($keyListItem);
$pool->expects(self::once())
->method('deleteItems')
->with(self::callback(function ($keys) {
return count($keys) === 2 && in_array('backup_a', $keys) && in_array('backup_b', $keys);
}))
->willReturn(true);
$pool->expects(self::once())->method('saveDeferred')->willReturn(true);
$pool->expects(self::once())->method('commit')->willReturn(true);
$manager = $this->createManager($pool);
$manager->expire();
}
public function testExpireDoesNothingWhenNoBackupCodes(): void {
$pool = $this->createMockPool();
$keyListItem = $this->createMockItem('__key_list', ['other' => true], true);
$changeListItem = $this->createMockItem('__chg_list', [], true);
$pool->method('getItems')
->with(['__key_list', '__chg_list'])
->willReturn([$keyListItem, $changeListItem]);
$pool->method('getItem')
->with('__key_list')
->willReturn($keyListItem);
$pool->expects(self::never())->method('deleteItems');
$manager = $this->createManager($pool);
$manager->expire();
}
}
+126
View File
@@ -0,0 +1,126 @@
<?php
declare(strict_types=1);
namespace App\Tests\Unit\Service;
use App\Service\DomainManager;
use PHPUnit\Framework\TestCase;
final class DomainManagerTest extends TestCase {
public function testGetAuthSubdomainWhenEnabled(): void {
$dm = new DomainManager(true, 'auth.example.com');
self::assertSame('auth.example.com', $dm->getAuthSubdomain());
}
public function testGetAuthSubdomainWhenDisabled(): void {
$dm = new DomainManager(false, 'auth.example.com');
self::assertNull($dm->getAuthSubdomain());
}
public function testGetAuthSubdomainWithEmptySubdomain(): void {
$dm = new DomainManager(true, '');
self::assertNull($dm->getAuthSubdomain());
}
public function testAuthBaseWithSubdomain(): void {
$dm = new DomainManager(true, 'auth.example.com');
self::assertSame('example.com', $dm->authBase());
}
public function testAuthBaseWithoutSubdomain(): void {
$dm = new DomainManager(false, 'auth.example.com');
self::assertNull($dm->authBase());
}
public function testAuthBaseWithEmptySubdomain(): void {
$dm = new DomainManager(true, '');
self::assertNull($dm->authBase());
}
public function testAuthBaseWithLocalhost(): void {
$dm = new DomainManager(true, 'localhost');
self::assertNull($dm->authBase());
}
public function testAuthBaseWithIp(): void {
$dm = new DomainManager(true, '192.168.1.1');
self::assertNull($dm->authBase());
}
public function testMatchesAuthWithSameBase(): void {
$dm = new DomainManager(true, 'auth.example.com');
self::assertTrue($dm->matchesAuth('www.example.com'));
self::assertTrue($dm->matchesAuth('app.example.com'));
}
public function testMatchesAuthWithDifferentBase(): void {
$dm = new DomainManager(true, 'auth.example.com');
self::assertFalse($dm->matchesAuth('other.com'));
}
public function testMatchesAuthWithIp(): void {
$dm = new DomainManager(true, 'auth.example.com');
self::assertFalse($dm->matchesAuth('192.168.1.1'));
}
public function testMatchesAuthWithLocalhost(): void {
$dm = new DomainManager(true, 'auth.example.com');
self::assertFalse($dm->matchesAuth('localhost'));
}
public function testMatchesAuthWhenDisabled(): void {
$dm = new DomainManager(false, 'auth.example.com');
self::assertFalse($dm->matchesAuth('www.example.com'));
}
public function testMatchesAuthWithCoUk(): void {
$dm = new DomainManager(true, 'auth.example.co.uk');
self::assertTrue($dm->matchesAuth('www.example.co.uk'));
self::assertFalse($dm->matchesAuth('other.co.uk'));
}
public function testValidReturnWithValidUrl(): void {
$dm = new DomainManager(false, '');
self::assertTrue($dm->validReturn('https://example.com/path'));
}
public function testValidReturnWithInvalidUrl(): void {
$dm = new DomainManager(false, '');
self::assertFalse($dm->validReturn('not-a-url'));
}
public function testValidReturnWithEmptyString(): void {
$dm = new DomainManager(false, '');
self::assertFalse($dm->validReturn(''));
}
public function testValidReturnWithAuthSubdomainMatching(): void {
$dm = new DomainManager(true, 'auth.example.com');
self::assertTrue($dm->validReturn('https://www.example.com/path'));
}
public function testValidReturnWithAuthSubdomainNotMatching(): void {
$dm = new DomainManager(true, 'auth.example.com');
self::assertFalse($dm->validReturn('https://evil.com/path'));
}
public function testValidReturnWithNoHost(): void {
$dm = new DomainManager(true, 'auth.example.com');
self::assertFalse($dm->validReturn('javascript:void(0)'));
}
public function testMatchesAuthWithMultiPartTld(): void {
$dm = new DomainManager(true, 'auth.example.com.br');
self::assertTrue($dm->matchesAuth('www.example.com.br'));
}
public function testMatchesAuthWithUkCo(): void {
$dm = new DomainManager(true, 'auth.example.co.uk');
self::assertTrue($dm->matchesAuth('www.example.co.uk'));
}
public function testMatchesAuthWithOrgUk(): void {
$dm = new DomainManager(true, 'auth.example.org.uk');
self::assertTrue($dm->matchesAuth('www.example.org.uk'));
}
}
+345
View File
@@ -0,0 +1,345 @@
<?php
declare(strict_types=1);
namespace App\Tests\Unit\Service;
use App\ConfigBag;
use App\Data\Payload;
use App\Enum\Scope;
use App\Service\BackupCodeManager;
use App\Service\DomainManager;
use App\Service\LoginManager;
use OTPHP\TOTP;
use PHPUnit\Framework\TestCase;
use Psr\Cache\CacheItemInterface;
use Psr\Cache\CacheItemPoolInterface;
use Psr\Clock\ClockInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\HttpException;
final class LoginManagerTest extends TestCase {
private function createMockItem(string $key, mixed $value = null, bool $isHit = true): CacheItemInterface {
$item = $this->createMock(CacheItemInterface::class);
$item->method('getKey')->willReturn($key);
$item->method('get')->willReturn($value);
$item->method('isHit')->willReturn($isHit);
$item->method('set')->willReturnSelf();
$item->method('expiresAfter')->willReturnSelf();
return $item;
}
private function createConfigBag(int $ipTtl = 1800): ConfigBag {
$clock = $this->createMock(ClockInterface::class);
$utilities = $this->createMock(\App\Utilities::class);
$totp = TOTP::generate($clock);
$totp->setLabel('Test');
return new ConfigBag(
$utilities,
$clock,
3600,
$totp->getProvisioningUri(),
$ipTtl,
false,
'Error',
'Teapot',
'Too Many'
);
}
private function createManager(
CacheItemPoolInterface $cache,
?BackupCodeManager $backupCodeManager = null,
?DomainManager $domainManager = null
): LoginManager {
$bcm = $backupCodeManager ?? $this->createMock(BackupCodeManager::class);
$dm = $domainManager ?? new DomainManager(false, '');
$manager = new LoginManager($cache, $bcm, $dm);
$manager->setConfig($this->createConfigBag());
$manager->setLogger($this->createMock(LoggerInterface::class));
$nonceCache = $this->createMock(CacheItemPoolInterface::class);
$nonceItem = $this->createMockItem('nonce_abc', true, true);
$nonceCache->method('getItem')->willReturn($nonceItem);
$nonceCache->method('save')->willReturn(true);
$manager->setNonceCache($nonceCache);
return $manager;
}
public function testCheckTokenWithValidTotpAndCookieScope(): void {
$cache = $this->createMock(CacheItemPoolInterface::class);
$keyListItem = $this->createMockItem('__key_list', [], true);
$changeListItem = $this->createMockItem('__chg_list', [], true);
$cache->method('getItems')
->with(['__key_list', '__chg_list'])
->willReturn([$keyListItem, $changeListItem]);
$cache->method('getItem')
->willReturnCallback(function ($key) {
if (str_starts_with($key, 'cookie_')) {
return $this->createMockItem($key, null, false);
}
return $this->createMockItem($key, null, false);
});
$cache->method('saveDeferred')->willReturn(true);
$cache->method('commit')->willReturn(true);
$cache->method('save')->willReturn(true);
$manager = $this->createManager($cache);
$clock = $this->createMock(ClockInterface::class);
$totp = TOTP::generate($clock);
$totp->setLabel('Test');
$payload = new Payload();
$payload->id = 'user1';
$payload->token = $totp->now();
$payload->nonce = 'abc';
$payload->json = true;
$payload->scope = Scope::Cookie;
$request = Request::create('https://example.com/');
$response = $manager->checkToken($payload, $request);
self::assertNotNull($response);
self::assertSame(303, $response->getStatusCode());
self::assertStringContainsString('Login successful', $response->getContent());
self::assertTrue($response->headers->hasCookie('__Host-Http-Preauth'));
}
public function testCheckTokenWithValidTotpAndNoneScope(): void {
$cache = $this->createMock(CacheItemPoolInterface::class);
$keyListItem = $this->createMockItem('__key_list', [], true);
$changeListItem = $this->createMockItem('__chg_list', [], true);
$cache->method('getItems')
->with(['__key_list', '__chg_list'])
->willReturn([$keyListItem, $changeListItem]);
$manager = $this->createManager($cache);
$clock = $this->createMock(ClockInterface::class);
$totp = TOTP::generate($clock);
$totp->setLabel('Test');
$payload = new Payload();
$payload->id = 'user1';
$payload->token = $totp->now();
$payload->nonce = 'abc';
$payload->json = false;
$payload->scope = Scope::None;
$request = Request::create('https://example.com/');
$response = $manager->checkToken($payload, $request);
self::assertNotNull($response);
self::assertSame(200, $response->getStatusCode());
self::assertSame('hi user1', $response->getContent());
self::assertFalse($response->headers->hasCookie('__Host-Http-Preauth'));
}
public function testCheckTokenWithInvalidToken(): void {
$cache = $this->createMock(CacheItemPoolInterface::class);
$keyListItem = $this->createMockItem('__key_list', [], true);
$changeListItem = $this->createMockItem('__chg_list', [], true);
$cache->method('getItems')
->with(['__key_list', '__chg_list'])
->willReturn([$keyListItem, $changeListItem]);
$manager = $this->createManager($cache);
$payload = new Payload();
$payload->id = 'user1';
$payload->token = 'invalid';
$payload->nonce = 'abc';
$payload->json = true;
$payload->scope = Scope::Cookie;
$request = Request::create('https://example.com/');
$response = $manager->checkToken($payload, $request);
self::assertNull($response);
}
public function testCheckTokenWithValidBackupCode(): void {
$cache = $this->createMock(CacheItemPoolInterface::class);
$keyListItem = $this->createMockItem('__key_list', [], true);
$changeListItem = $this->createMockItem('__chg_list', [], true);
$cache->method('getItems')
->with(['__key_list', '__chg_list'])
->willReturn([$keyListItem, $changeListItem]);
$cache->method('getItem')
->willReturnCallback(function ($key) {
if (str_starts_with($key, 'cookie_')) {
return $this->createMockItem($key, null, false);
}
return $this->createMockItem($key, null, false);
});
$cache->method('saveDeferred')->willReturn(true);
$cache->method('commit')->willReturn(true);
$cache->method('save')->willReturn(true);
$backupManager = $this->createMock(BackupCodeManager::class);
$backupManager->method('verifyAndConsume')->willReturn(true);
$manager = $this->createManager($cache, $backupManager);
$payload = new Payload();
$payload->id = 'user1';
$payload->token = 'backup123';
$payload->nonce = 'abc';
$payload->json = true;
$payload->scope = Scope::Cookie;
$request = Request::create('https://example.com/');
$response = $manager->checkToken($payload, $request);
self::assertNotNull($response);
}
public function testCheckTokenWithIpScopeWhenDisabled(): void {
$cache = $this->createMock(CacheItemPoolInterface::class);
$keyListItem = $this->createMockItem('__key_list', [], true);
$changeListItem = $this->createMockItem('__chg_list', [], true);
$cache->method('getItems')
->with(['__key_list', '__chg_list'])
->willReturn([$keyListItem, $changeListItem]);
$cache->method('getItem')
->willReturnCallback(function ($key) {
if (str_starts_with($key, 'cookie_')) {
return $this->createMockItem($key, null, false);
}
return $this->createMockItem($key, null, false);
});
$cache->method('saveDeferred')->willReturn(true);
$cache->method('commit')->willReturn(true);
$cache->method('save')->willReturn(true);
$manager = $this->createManager($cache);
$clock = $this->createMock(ClockInterface::class);
$totp = TOTP::generate($clock);
$totp->setLabel('Test');
$payload = new Payload();
$payload->id = 'user1';
$payload->token = $totp->now();
$payload->nonce = 'abc';
$payload->json = true;
$payload->scope = Scope::Ip;
$request = Request::create('https://example.com/');
$response = $manager->checkToken($payload, $request);
self::assertNotNull($response);
// When ipTtl is 0, scope falls back to Cookie
self::assertTrue($response->headers->hasCookie('__Host-Http-Preauth'));
}
public function testCheckTokenWithInvalidNonce(): void {
$cache = $this->createMock(CacheItemPoolInterface::class);
$keyListItem = $this->createMockItem('__key_list', [], true);
$changeListItem = $this->createMockItem('__chg_list', [], true);
$cache->method('getItems')
->with(['__key_list', '__chg_list'])
->willReturn([$keyListItem, $changeListItem]);
$manager = $this->createManager($cache);
$nonceCache = $this->createMock(CacheItemPoolInterface::class);
$nonceItem = $this->createMockItem('nonce_bad', false, true);
$nonceCache->method('getItem')->willReturn($nonceItem);
$manager->setNonceCache($nonceCache);
$clock = $this->createMock(ClockInterface::class);
$totp = TOTP::generate($clock);
$totp->setLabel('Test');
$payload = new Payload();
$payload->id = 'user1';
$payload->token = $totp->now();
$payload->nonce = 'bad';
$payload->json = true;
$payload->scope = Scope::None;
$request = Request::create('https://example.com/');
$response = $manager->checkToken($payload, $request);
self::assertNull($response);
}
public function testCheckTokenWithMissingNonce(): void {
$cache = $this->createMock(CacheItemPoolInterface::class);
$keyListItem = $this->createMockItem('__key_list', [], true);
$changeListItem = $this->createMockItem('__chg_list', [], true);
$cache->method('getItems')
->with(['__key_list', '__chg_list'])
->willReturn([$keyListItem, $changeListItem]);
$manager = $this->createManager($cache);
$nonceCache = $this->createMock(CacheItemPoolInterface::class);
$nonceItem = $this->createMockItem('nonce_missing', null, false);
$nonceCache->method('getItem')->willReturn($nonceItem);
$manager->setNonceCache($nonceCache);
$clock = $this->createMock(ClockInterface::class);
$totp = TOTP::generate($clock);
$totp->setLabel('Test');
$payload = new Payload();
$payload->id = 'user1';
$payload->token = $totp->now();
$payload->nonce = 'missing';
$payload->json = true;
$payload->scope = Scope::None;
$request = Request::create('https://example.com/');
$response = $manager->checkToken($payload, $request);
self::assertNull($response);
}
public function testCheckTokenWithUlidCollisionThrows(): void {
$cache = $this->createMock(CacheItemPoolInterface::class);
$keyListItem = $this->createMockItem('__key_list', [], true);
$changeListItem = $this->createMockItem('__chg_list', [], true);
$cache->method('getItems')
->with(['__key_list', '__chg_list'])
->willReturn([$keyListItem, $changeListItem]);
$cache->method('getItem')
->willReturnCallback(function ($key) {
if (str_starts_with($key, 'cookie_')) {
// Simulate collision
return $this->createMockItem($key, 'existing', true);
}
return $this->createMockItem($key, null, false);
});
$manager = $this->createManager($cache);
$clock = $this->createMock(ClockInterface::class);
$totp = TOTP::generate($clock);
$totp->setLabel('Test');
$payload = new Payload();
$payload->id = 'user1';
$payload->token = $totp->now();
$payload->nonce = 'abc';
$payload->json = true;
$payload->scope = Scope::Cookie;
$request = Request::create('https://example.com/');
$this->expectException(HttpException::class);
$this->expectExceptionMessage('Internal Server Error');
$manager->checkToken($payload, $request);
}
}
+23
View File
@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace App\Tests\Unit\Trait;
use App\Trait\CookieNameTrait;
use PHPUnit\Framework\TestCase;
final class CookieNameTraitTest extends TestCase {
use CookieNameTrait;
public function testCookieName(): void {
self::assertSame('__Host-Http-Preauth', $this->cookieName());
}
public function testAuthCookieName(): void {
self::assertSame('__Http-Domain-Preauth', $this->authCookieName());
}
public function testHeaderName(): void {
self::assertSame('X-Preauth', $this->headerName());
}
}
+40
View File
@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace App\Tests\Unit\Trait;
use App\Trait\StringTrait;
use PHPUnit\Framework\TestCase;
final class StringTraitTest extends TestCase {
use StringTrait;
public function testMakeCacheKeyWithAlphanumeric(): void {
self::assertSame('hello', $this->makeCacheKey('hello'));
}
public function testMakeCacheKeyReplacesInvalidChars(): void {
self::assertSame('hello_world', $this->makeCacheKey('hello world'));
self::assertSame('a_b_c_', $this->makeCacheKey('a!b@c#'));
self::assertSame('test_key', $this->makeCacheKey('test-key'));
}
public function testMakeCacheKeyTruncatesTo128(): void {
$long = str_repeat('a', 200);
self::assertSame(128, mb_strlen($this->makeCacheKey($long)));
self::assertSame(str_repeat('a', 128), $this->makeCacheKey($long));
}
public function testMakeCacheKeyWithUnicode(): void {
self::assertSame('_', $this->makeCacheKey('日本語'));
self::assertSame('hello_world', $this->makeCacheKey('hello日本語world'));
}
public function testMakeCacheKeyWithDotsAndUnderscores(): void {
self::assertSame('key.name_1', $this->makeCacheKey('key.name_1'));
}
public function testMakeCacheKeyEmptyString(): void {
self::assertSame('', $this->makeCacheKey(''));
}
}
+13
View File
@@ -0,0 +1,13 @@
<?php
use Symfony\Component\Dotenv\Dotenv;
require dirname(__DIR__).'/vendor/autoload.php';
if (method_exists(Dotenv::class, 'bootEnv')) {
(new Dotenv())->bootEnv(dirname(__DIR__).'/.env');
}
if ($_SERVER['APP_DEBUG'] ?? false) {
umask(0000);
}