chore: add php-cs-fixer with PSR-12 config and CI check
- Add friendsofphp/php-cs-fixer to require-dev - Create .php-cs-fixer.dist.php configured for @PSR12 ruleset - Add php-cs-fixer dry-run step to CI pipeline - Auto-fix existing PSR-12 violations - Document code style tooling in readme.md
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Unit;
|
||||
@@ -6,8 +7,10 @@ namespace App\Tests\Unit;
|
||||
use App\Clock;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class ClockTest extends TestCase {
|
||||
public function testNowReturnsDateTimeImmutable(): void {
|
||||
final class ClockTest extends TestCase
|
||||
{
|
||||
public function testNowReturnsDateTimeImmutable(): void
|
||||
{
|
||||
$clock = new Clock();
|
||||
$before = new \DateTimeImmutable();
|
||||
$now = $clock->now();
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Unit\Command;
|
||||
@@ -10,25 +11,29 @@ use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Cache\Adapter\ArrayAdapter;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
|
||||
final class GenerateBackupCodesCommandTest extends TestCase {
|
||||
|
||||
final class GenerateBackupCodesCommandTest extends TestCase
|
||||
{
|
||||
/** PersistCache is final, so construct a real one backed by ArrayAdapters. */
|
||||
private function makePersistCache(): PersistCache {
|
||||
private function makePersistCache(): PersistCache
|
||||
{
|
||||
return new PersistCache(new ArrayAdapter(), new ArrayAdapter());
|
||||
}
|
||||
|
||||
/** A stub BackupCodeInterface that returns the given codes from generate(). */
|
||||
private function makeManagerStub(array $generatedCodes): BackupCodeInterface {
|
||||
private function makeManagerStub(array $generatedCodes): BackupCodeInterface
|
||||
{
|
||||
$manager = $this->createStub(BackupCodeInterface::class);
|
||||
$manager->method('generate')->willReturn($generatedCodes);
|
||||
return $manager;
|
||||
}
|
||||
|
||||
public function testGenerateDefaultCountOutputsCodes(): void {
|
||||
public function testGenerateDefaultCountOutputsCodes(): void
|
||||
{
|
||||
$codes = ['abc123', 'def456', 'ghi789', 'jkl012', 'mno345',
|
||||
'pqr678', 'stu901', 'vwx234', 'yzA567', 'bCd890'];
|
||||
$command = new GenerateBackupCodesCommand(
|
||||
$this->makeManagerStub($codes), $this->makePersistCache()
|
||||
$this->makeManagerStub($codes),
|
||||
$this->makePersistCache()
|
||||
);
|
||||
$command->setName('app:generate-backup-codes');
|
||||
|
||||
@@ -42,7 +47,8 @@ final class GenerateBackupCodesCommandTest extends TestCase {
|
||||
}
|
||||
}
|
||||
|
||||
public function testGenerateSpecificCountPassesCountToManager(): void {
|
||||
public function testGenerateSpecificCountPassesCountToManager(): void
|
||||
{
|
||||
$manager = $this->createMock(BackupCodeInterface::class);
|
||||
$manager->expects(self::once())
|
||||
->method('generate')
|
||||
@@ -58,7 +64,8 @@ final class GenerateBackupCodesCommandTest extends TestCase {
|
||||
self::assertSame(0, $exit);
|
||||
}
|
||||
|
||||
public function testDefaultCountArgumentIsTen(): void {
|
||||
public function testDefaultCountArgumentIsTen(): void
|
||||
{
|
||||
// the configured default for the count argument should be 10
|
||||
$manager = $this->createMock(BackupCodeInterface::class);
|
||||
$manager->expects(self::once())
|
||||
@@ -76,13 +83,15 @@ final class GenerateBackupCodesCommandTest extends TestCase {
|
||||
$this->addToAssertionCount(1);
|
||||
}
|
||||
|
||||
public function testBootsAndPersistsCache(): void {
|
||||
public function testBootsAndPersistsCache(): void
|
||||
{
|
||||
// PersistCache is final and can't be mocked, but we can verify the
|
||||
// command runs end-to-end with a real instance; boot()/persist()
|
||||
// are invoked implicitly. A successful exit confirms both were called
|
||||
// without throwing.
|
||||
$command = new GenerateBackupCodesCommand(
|
||||
$this->makeManagerStub(['code1']), $this->makePersistCache()
|
||||
$this->makeManagerStub(['code1']),
|
||||
$this->makePersistCache()
|
||||
);
|
||||
$command->setName('app:generate-backup-codes');
|
||||
|
||||
@@ -92,9 +101,11 @@ final class GenerateBackupCodesCommandTest extends TestCase {
|
||||
self::assertSame(0, $exit);
|
||||
}
|
||||
|
||||
public function testZeroCodesOutputsNothing(): void {
|
||||
public function testZeroCodesOutputsNothing(): void
|
||||
{
|
||||
$command = new GenerateBackupCodesCommand(
|
||||
$this->makeManagerStub([]), $this->makePersistCache()
|
||||
$this->makeManagerStub([]),
|
||||
$this->makePersistCache()
|
||||
);
|
||||
$command->setName('app:generate-backup-codes');
|
||||
|
||||
@@ -105,9 +116,11 @@ final class GenerateBackupCodesCommandTest extends TestCase {
|
||||
self::assertSame('', trim($tester->getDisplay()));
|
||||
}
|
||||
|
||||
public function testCommandNameAndDescriptionAreConfigured(): void {
|
||||
public function testCommandNameAndDescriptionAreConfigured(): void
|
||||
{
|
||||
$command = new GenerateBackupCodesCommand(
|
||||
$this->makeManagerStub([]), $this->makePersistCache()
|
||||
$this->makeManagerStub([]),
|
||||
$this->makePersistCache()
|
||||
);
|
||||
// configuring via the Application runs the protected configure()
|
||||
$app = new \Symfony\Component\Console\Application();
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Unit;
|
||||
@@ -10,8 +11,10 @@ use Psr\Cache\CacheItemInterface;
|
||||
use Psr\Cache\CacheItemPoolInterface;
|
||||
use Psr\Clock\ClockInterface;
|
||||
|
||||
final class ConfigBagTest extends TestCase {
|
||||
private function createUtilities(?string $totp = null): Utilities {
|
||||
final class ConfigBagTest extends TestCase
|
||||
{
|
||||
private function createUtilities(?string $totp = null): Utilities
|
||||
{
|
||||
$clock = $this->createStub(ClockInterface::class);
|
||||
$cache = $this->createStub(CacheItemPoolInterface::class);
|
||||
|
||||
@@ -28,14 +31,21 @@ final class ConfigBagTest extends TestCase {
|
||||
return new Utilities($clock, $cache);
|
||||
}
|
||||
|
||||
public function testGettersWithExplicitValues(): void {
|
||||
public function testGettersWithExplicitValues(): void
|
||||
{
|
||||
$clock = $this->createStub(ClockInterface::class);
|
||||
$utilities = $this->createUtilities();
|
||||
|
||||
$config = new ConfigBag(
|
||||
$utilities, $clock,
|
||||
3600, 'otpauth://totp/test', 1800, true,
|
||||
'Error!', 'Teapot!', 'Too Many!'
|
||||
$utilities,
|
||||
$clock,
|
||||
3600,
|
||||
'otpauth://totp/test',
|
||||
1800,
|
||||
true,
|
||||
'Error!',
|
||||
'Teapot!',
|
||||
'Too Many!'
|
||||
);
|
||||
|
||||
self::assertSame($clock, $config->clock());
|
||||
@@ -48,43 +58,63 @@ final class ConfigBagTest extends TestCase {
|
||||
self::assertSame('Too Many!', $config->tooManyTitle());
|
||||
}
|
||||
|
||||
public function testTotpUriFallsBackToUtilitiesWhenEmpty(): void {
|
||||
public function testTotpUriFallsBackToUtilitiesWhenEmpty(): void
|
||||
{
|
||||
$clock = $this->createStub(ClockInterface::class);
|
||||
$utilities = $this->createUtilities('fallback-totp');
|
||||
|
||||
$config = new ConfigBag(
|
||||
$utilities, $clock,
|
||||
3600, '', 1800, false,
|
||||
'Error', 'Teapot', 'Too Many'
|
||||
$utilities,
|
||||
$clock,
|
||||
3600,
|
||||
'',
|
||||
1800,
|
||||
false,
|
||||
'Error',
|
||||
'Teapot',
|
||||
'Too Many'
|
||||
);
|
||||
|
||||
self::assertSame('fallback-totp', $config->totpUri());
|
||||
}
|
||||
|
||||
public function testIpTtlFallsBackToNullWhenZero(): void {
|
||||
public function testIpTtlFallsBackToNullWhenZero(): void
|
||||
{
|
||||
$clock = $this->createStub(ClockInterface::class);
|
||||
$utilities = $this->createUtilities();
|
||||
|
||||
$config = new ConfigBag(
|
||||
$utilities, $clock,
|
||||
3600, 'otpauth://totp/test', 0, false,
|
||||
'Error', 'Teapot', 'Too Many'
|
||||
$utilities,
|
||||
$clock,
|
||||
3600,
|
||||
'otpauth://totp/test',
|
||||
0,
|
||||
false,
|
||||
'Error',
|
||||
'Teapot',
|
||||
'Too Many'
|
||||
);
|
||||
|
||||
self::assertNull($config->ipTtl());
|
||||
}
|
||||
|
||||
public function testIpTtlFallsBackToNullWhenNull(): void {
|
||||
public function testIpTtlFallsBackToNullWhenNull(): void
|
||||
{
|
||||
$clock = $this->createStub(ClockInterface::class);
|
||||
$utilities = $this->createUtilities();
|
||||
|
||||
$config = new ConfigBag(
|
||||
$utilities, $clock,
|
||||
3600, 'otpauth://totp/test', null, false,
|
||||
'Error', 'Teapot', 'Too Many'
|
||||
$utilities,
|
||||
$clock,
|
||||
3600,
|
||||
'otpauth://totp/test',
|
||||
null,
|
||||
false,
|
||||
'Error',
|
||||
'Teapot',
|
||||
'Too Many'
|
||||
);
|
||||
|
||||
self::assertNull($config->ipTtl());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Unit\Data;
|
||||
@@ -8,12 +9,15 @@ use App\Enum\Scope;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\HttpFoundation\InputBag;
|
||||
|
||||
final class PayloadTest extends TestCase {
|
||||
private static function b64u(string $data): string {
|
||||
final class PayloadTest extends TestCase
|
||||
{
|
||||
private static function b64u(string $data): string
|
||||
{
|
||||
return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
|
||||
}
|
||||
|
||||
public function testDecodeValidBase64Url(): void {
|
||||
public function testDecodeValidBase64Url(): void
|
||||
{
|
||||
$data = json_encode([
|
||||
'id' => 'testuser', 'token' => '123456', 'nonce' => 'abc123',
|
||||
'json' => true, 'scope' => 'cookie',
|
||||
@@ -28,41 +32,50 @@ final class PayloadTest extends TestCase {
|
||||
self::assertSame(Scope::Cookie, $payload->scope);
|
||||
}
|
||||
|
||||
public function testDecodeInvalidBase64UrlReturnsNull(): void {
|
||||
public function testDecodeInvalidBase64UrlReturnsNull(): void
|
||||
{
|
||||
self::assertNull(Payload::decode('!!!not-valid-base64!!!'));
|
||||
}
|
||||
|
||||
public function testDecodeNonObjectJsonReturnsNull(): void {
|
||||
public function testDecodeNonObjectJsonReturnsNull(): void
|
||||
{
|
||||
self::assertNull(Payload::decode(self::b64u('"just a string"')));
|
||||
}
|
||||
|
||||
public function testDecodeInvalidJsonReturnsNull(): void {
|
||||
public function testDecodeInvalidJsonReturnsNull(): void
|
||||
{
|
||||
// valid base64url but invalid JSON
|
||||
self::assertNull(Payload::decode(self::b64u('{invalid json')));
|
||||
}
|
||||
|
||||
public function testDecodeJsonArrayReturnsNull(): void {
|
||||
public function testDecodeJsonArrayReturnsNull(): void
|
||||
{
|
||||
self::assertNull(Payload::decode(self::b64u('[1,2,3]')));
|
||||
}
|
||||
|
||||
public function testDecodeJsonNullReturnsNull(): void {
|
||||
public function testDecodeJsonNullReturnsNull(): void
|
||||
{
|
||||
self::assertNull(Payload::decode(self::b64u('null')));
|
||||
}
|
||||
|
||||
public function testDecodeJsonBooleanReturnsNull(): void {
|
||||
public function testDecodeJsonBooleanReturnsNull(): void
|
||||
{
|
||||
self::assertNull(Payload::decode(self::b64u('true')));
|
||||
self::assertNull(Payload::decode(self::b64u('false')));
|
||||
}
|
||||
|
||||
public function testDecodeJsonNumberReturnsNull(): void {
|
||||
public function testDecodeJsonNumberReturnsNull(): void
|
||||
{
|
||||
self::assertNull(Payload::decode(self::b64u('42')));
|
||||
}
|
||||
|
||||
public function testDecodeEmptyStringReturnsNull(): void {
|
||||
public function testDecodeEmptyStringReturnsNull(): void
|
||||
{
|
||||
self::assertNull(Payload::decode(''));
|
||||
}
|
||||
|
||||
public function testLoadWithValidInputBag(): void {
|
||||
public function testLoadWithValidInputBag(): void
|
||||
{
|
||||
$input = new InputBag([
|
||||
'username' => 'alice', 'nonce' => 'nonce123', 'totp' => '654321',
|
||||
]);
|
||||
@@ -76,28 +89,33 @@ final class PayloadTest extends TestCase {
|
||||
self::assertSame(Scope::Cookie, $payload->scope);
|
||||
}
|
||||
|
||||
public function testLoadMissingUsernameReturnsNull(): void {
|
||||
public function testLoadMissingUsernameReturnsNull(): void
|
||||
{
|
||||
$input = new InputBag(['nonce' => 'n', 'totp' => 't']);
|
||||
self::assertNull(Payload::load($input));
|
||||
}
|
||||
|
||||
public function testLoadMissingNonceReturnsNull(): void {
|
||||
public function testLoadMissingNonceReturnsNull(): void
|
||||
{
|
||||
$input = new InputBag(['username' => 'u', 'totp' => 't']);
|
||||
self::assertNull(Payload::load($input));
|
||||
}
|
||||
|
||||
public function testLoadMissingTotpReturnsNull(): void {
|
||||
public function testLoadMissingTotpReturnsNull(): void
|
||||
{
|
||||
$input = new InputBag(['username' => 'u', 'nonce' => 'n']);
|
||||
self::assertNull(Payload::load($input));
|
||||
}
|
||||
|
||||
public function testLoadWithAllFieldsPresentButEmptyReturnsNull(): void {
|
||||
public function testLoadWithAllFieldsPresentButEmptyReturnsNull(): void
|
||||
{
|
||||
// has() returns true for all, but create() rejects empty values
|
||||
$input = new InputBag(['username' => '', 'nonce' => '', 'totp' => '']);
|
||||
self::assertNull(Payload::load($input));
|
||||
}
|
||||
|
||||
public function testCreateWithValidData(): void {
|
||||
public function testCreateWithValidData(): void
|
||||
{
|
||||
$data = (object)[
|
||||
'id' => 'user1', 'token' => 'tok1', 'nonce' => 'non1',
|
||||
'json' => false, 'scope' => 'ip',
|
||||
@@ -112,13 +130,15 @@ final class PayloadTest extends TestCase {
|
||||
self::assertSame(Scope::Ip, $payload->scope);
|
||||
}
|
||||
|
||||
public function testCreateWithDefaultScope(): void {
|
||||
public function testCreateWithDefaultScope(): void
|
||||
{
|
||||
$data = (object)['id' => 'user1', 'token' => 'tok1', 'nonce' => 'non1'];
|
||||
$payload = Payload::create($data);
|
||||
self::assertSame(Scope::Cookie, $payload->scope);
|
||||
}
|
||||
|
||||
public function testCreateWithInvalidScopeFallsBackToCookie(): void {
|
||||
public function testCreateWithInvalidScopeFallsBackToCookie(): void
|
||||
{
|
||||
$data = (object)[
|
||||
'id' => 'user1', 'token' => 'tok1', 'nonce' => 'non1',
|
||||
'scope' => 'admin',
|
||||
@@ -127,13 +147,15 @@ final class PayloadTest extends TestCase {
|
||||
self::assertSame(Scope::Cookie, $payload->scope);
|
||||
}
|
||||
|
||||
public function testCreateWithMissingJsonDefaultsToTrue(): void {
|
||||
public function testCreateWithMissingJsonDefaultsToTrue(): void
|
||||
{
|
||||
$data = (object)['id' => 'user1', 'token' => 'tok1', 'nonce' => 'non1'];
|
||||
$payload = Payload::create($data);
|
||||
self::assertTrue($payload->json);
|
||||
}
|
||||
|
||||
public function testCreateWithNoneScopeSetsJsonFalse(): void {
|
||||
public function testCreateWithNoneScopeSetsJsonFalse(): void
|
||||
{
|
||||
$data = (object)[
|
||||
'id' => 'user1', 'token' => 'tok1', 'nonce' => 'non1',
|
||||
'json' => true, 'scope' => 'none',
|
||||
@@ -143,27 +165,32 @@ final class PayloadTest extends TestCase {
|
||||
self::assertFalse($payload->json);
|
||||
}
|
||||
|
||||
public function testCreateWithEmptyIdReturnsNull(): void {
|
||||
public function testCreateWithEmptyIdReturnsNull(): void
|
||||
{
|
||||
$data = (object)['id' => '', 'token' => 't', 'nonce' => 'n'];
|
||||
self::assertNull(Payload::create($data));
|
||||
}
|
||||
|
||||
public function testCreateWithWhitespaceIdReturnsNull(): void {
|
||||
public function testCreateWithWhitespaceIdReturnsNull(): void
|
||||
{
|
||||
$data = (object)['id' => ' ', 'token' => 't', 'nonce' => 'n'];
|
||||
self::assertNull(Payload::create($data));
|
||||
}
|
||||
|
||||
public function testCreateWithEmptyTokenReturnsNull(): void {
|
||||
public function testCreateWithEmptyTokenReturnsNull(): void
|
||||
{
|
||||
$data = (object)['id' => 'u', 'token' => '', 'nonce' => 'n'];
|
||||
self::assertNull(Payload::create($data));
|
||||
}
|
||||
|
||||
public function testCreateWithEmptyNonceReturnsNull(): void {
|
||||
public function testCreateWithEmptyNonceReturnsNull(): void
|
||||
{
|
||||
$data = (object)['id' => 'u', 'token' => 't', 'nonce' => ''];
|
||||
self::assertNull(Payload::create($data));
|
||||
}
|
||||
|
||||
public function testCreateTrimsAndTruncatesFields(): void {
|
||||
public function testCreateTrimsAndTruncatesFields(): void
|
||||
{
|
||||
$long = str_repeat('a', 200);
|
||||
$data = (object)[
|
||||
'id' => ' ' . $long . ' ',
|
||||
@@ -177,7 +204,8 @@ final class PayloadTest extends TestCase {
|
||||
self::assertSame($expected, $payload->nonce);
|
||||
}
|
||||
|
||||
public function testToString(): void {
|
||||
public function testToString(): void
|
||||
{
|
||||
$payload = new Payload();
|
||||
$payload->id = 'u';
|
||||
$payload->token = 't';
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Unit\Enum;
|
||||
@@ -6,20 +7,24 @@ namespace App\Tests\Unit\Enum;
|
||||
use App\Enum\Scope;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class ScopeTest extends TestCase {
|
||||
public function testCases(): void {
|
||||
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 {
|
||||
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 testTryFromInvalid(): void {
|
||||
public function testTryFromInvalid(): void
|
||||
{
|
||||
self::assertNull(Scope::tryFrom('invalid'));
|
||||
self::assertNull(Scope::tryFrom(''));
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Unit\Listener;
|
||||
@@ -14,19 +15,22 @@ use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Event\RequestEvent;
|
||||
use Symfony\Component\HttpKernel\HttpKernelInterface;
|
||||
|
||||
final class AcceptListenerTest extends TestCase {
|
||||
final class AcceptListenerTest extends TestCase
|
||||
{
|
||||
use TotpTestHelper;
|
||||
|
||||
private const string COOKIE_NAME = '__Host-Http-Preauth';
|
||||
private const string AUTH_COOKIE_NAME = '__Http-Domain-Preauth';
|
||||
|
||||
private function makeListener(ArrayAdapter $pool, DomainManager $domainManager): AcceptListener {
|
||||
private function makeListener(ArrayAdapter $pool, DomainManager $domainManager): AcceptListener
|
||||
{
|
||||
$listener = new AcceptListener($pool, $domainManager);
|
||||
$listener->setLogger(new NullLogger());
|
||||
return $listener;
|
||||
}
|
||||
|
||||
private function makeEvent(Request $request): RequestEvent {
|
||||
private function makeEvent(Request $request): RequestEvent
|
||||
{
|
||||
return new RequestEvent(
|
||||
$this->createStub(\Symfony\Component\HttpKernel\HttpKernelInterface::class),
|
||||
$request,
|
||||
@@ -36,7 +40,8 @@ final class AcceptListenerTest extends TestCase {
|
||||
|
||||
/* ── valid cookie session ─────────────────────────────────────────── */
|
||||
|
||||
public function testValidCookieSetsResponseWithRemoteUser(): void {
|
||||
public function testValidCookieSetsResponseWithRemoteUser(): void
|
||||
{
|
||||
$pool = new ArrayAdapter();
|
||||
$ulid = '01HXY1234567890ABCDEFGHIJK';
|
||||
$item = $pool->getItem('cookie_' . $ulid);
|
||||
@@ -59,7 +64,8 @@ final class AcceptListenerTest extends TestCase {
|
||||
self::assertSame('text/plain', $response->headers->get('Content-Type'));
|
||||
}
|
||||
|
||||
public function testValidCookieUsesAuthCookieNameWhenUsingCentralAuth(): void {
|
||||
public function testValidCookieUsesAuthCookieNameWhenUsingCentralAuth(): void
|
||||
{
|
||||
$pool = new ArrayAdapter();
|
||||
$ulid = '01HXY1234567890ABCDEFGHIJK';
|
||||
$item = $pool->getItem('cookie_' . $ulid);
|
||||
@@ -81,7 +87,8 @@ final class AcceptListenerTest extends TestCase {
|
||||
|
||||
/* ── negative cases ───────────────────────────────────────────────── */
|
||||
|
||||
public function testNoCookieSetsNoResponse(): void {
|
||||
public function testNoCookieSetsNoResponse(): void
|
||||
{
|
||||
$pool = new ArrayAdapter();
|
||||
$domainManager = new DomainManager(false, '');
|
||||
$listener = $this->makeListener($pool, $domainManager);
|
||||
@@ -92,7 +99,8 @@ final class AcceptListenerTest extends TestCase {
|
||||
self::assertFalse($event->hasResponse());
|
||||
}
|
||||
|
||||
public function testCookieWithoutSessionSetsNoResponse(): void {
|
||||
public function testCookieWithoutSessionSetsNoResponse(): void
|
||||
{
|
||||
$pool = new ArrayAdapter();
|
||||
$domainManager = new DomainManager(false, '');
|
||||
$listener = $this->makeListener($pool, $domainManager);
|
||||
@@ -106,7 +114,8 @@ final class AcceptListenerTest extends TestCase {
|
||||
self::assertFalse($event->hasResponse());
|
||||
}
|
||||
|
||||
public function testEmptyCookieValueSetsNoResponse(): void {
|
||||
public function testEmptyCookieValueSetsNoResponse(): void
|
||||
{
|
||||
$pool = new ArrayAdapter();
|
||||
$domainManager = new DomainManager(false, '');
|
||||
$listener = $this->makeListener($pool, $domainManager);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Unit\Listener;
|
||||
@@ -13,16 +14,19 @@ use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpKernel\Event\RequestEvent;
|
||||
use Symfony\Component\HttpKernel\HttpKernelInterface;
|
||||
|
||||
final class AllowListenerTest extends TestCase {
|
||||
final class AllowListenerTest extends TestCase
|
||||
{
|
||||
use TotpTestHelper;
|
||||
|
||||
private function makeListener(ArrayAdapter $pool, ConfigBag $config): AllowListener {
|
||||
private function makeListener(ArrayAdapter $pool, ConfigBag $config): AllowListener
|
||||
{
|
||||
$listener = new AllowListener($pool, $config);
|
||||
$listener->setLogger(new NullLogger());
|
||||
return $listener;
|
||||
}
|
||||
|
||||
private function makeEvent(Request $request): RequestEvent {
|
||||
private function makeEvent(Request $request): RequestEvent
|
||||
{
|
||||
return new RequestEvent(
|
||||
$this->createStub(HttpKernelInterface::class),
|
||||
$request,
|
||||
@@ -30,7 +34,8 @@ final class AllowListenerTest extends TestCase {
|
||||
);
|
||||
}
|
||||
|
||||
public function testValidIpSessionSetsResponseWithRemoteUser(): void {
|
||||
public function testValidIpSessionSetsResponseWithRemoteUser(): void
|
||||
{
|
||||
$pool = new ArrayAdapter();
|
||||
$item = $pool->getItem('ip_1.2.3.4');
|
||||
$item->set('carol');
|
||||
@@ -50,7 +55,8 @@ final class AllowListenerTest extends TestCase {
|
||||
self::assertSame('text/plain', $response->headers->get('Content-Type'));
|
||||
}
|
||||
|
||||
public function testNoIpSessionSetsNoResponse(): void {
|
||||
public function testNoIpSessionSetsNoResponse(): void
|
||||
{
|
||||
$pool = new ArrayAdapter();
|
||||
$config = $this->makeConfig(ipTtl: 1800);
|
||||
$listener = $this->makeListener($pool, $config);
|
||||
@@ -62,7 +68,8 @@ final class AllowListenerTest extends TestCase {
|
||||
self::assertFalse($event->hasResponse());
|
||||
}
|
||||
|
||||
public function testIpAccessDisabledSetsNoResponse(): void {
|
||||
public function testIpAccessDisabledSetsNoResponse(): void
|
||||
{
|
||||
$pool = new ArrayAdapter();
|
||||
// even though there's a stored session, ip access is disabled
|
||||
$item = $pool->getItem('ip_1.2.3.4');
|
||||
@@ -79,7 +86,8 @@ final class AllowListenerTest extends TestCase {
|
||||
self::assertFalse($event->hasResponse());
|
||||
}
|
||||
|
||||
public function testIpAccessDisabledDoesNotCheckCache(): void {
|
||||
public function testIpAccessDisabledDoesNotCheckCache(): void
|
||||
{
|
||||
$pool = new ArrayAdapter();
|
||||
$config = $this->makeConfig(ipTtl: 0);
|
||||
$listener = $this->makeListener($pool, $config);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Unit\Listener;
|
||||
@@ -16,7 +17,8 @@ use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Event\RequestEvent;
|
||||
use Symfony\Component\HttpKernel\HttpKernelInterface;
|
||||
|
||||
final class InterceptListenerTest extends TestCase {
|
||||
final class InterceptListenerTest extends TestCase
|
||||
{
|
||||
use ListenerTestHelper;
|
||||
|
||||
private const string COOKIE_NAME = '__Host-Http-Preauth';
|
||||
@@ -36,7 +38,8 @@ final class InterceptListenerTest extends TestCase {
|
||||
return $listener;
|
||||
}
|
||||
|
||||
private function makeEvent(Request $request): RequestEvent {
|
||||
private function makeEvent(Request $request): RequestEvent
|
||||
{
|
||||
return new RequestEvent(
|
||||
$this->createStub(HttpKernelInterface::class),
|
||||
$request,
|
||||
@@ -46,7 +49,8 @@ final class InterceptListenerTest extends TestCase {
|
||||
|
||||
/* ── central-auth redirect branch ─────────────────────────────────── */
|
||||
|
||||
public function testRedirectsToAuthSubdomainWhenHostMatchesBaseDomain(): void {
|
||||
public function testRedirectsToAuthSubdomainWhenHostMatchesBaseDomain(): void
|
||||
{
|
||||
$domainManager = new DomainManager(true, 'auth.example.com');
|
||||
$listener = $this->makeListener($domainManager);
|
||||
|
||||
@@ -64,7 +68,8 @@ final class InterceptListenerTest extends TestCase {
|
||||
self::assertStringContainsString(urlencode('https://app.example.com/dashboard'), $location);
|
||||
}
|
||||
|
||||
public function testDoesNotRedirectWhenAlreadyOnAuthSubdomain(): void {
|
||||
public function testDoesNotRedirectWhenAlreadyOnAuthSubdomain(): void
|
||||
{
|
||||
$domainManager = new DomainManager(true, 'auth.example.com');
|
||||
$listener = $this->makeListener($domainManager);
|
||||
|
||||
@@ -81,7 +86,8 @@ final class InterceptListenerTest extends TestCase {
|
||||
|
||||
/* ── login page rendering branch ──────────────────────────────────── */
|
||||
|
||||
public function testPresentsLoginPageWithUnauthorizedStatus(): void {
|
||||
public function testPresentsLoginPageWithUnauthorizedStatus(): void
|
||||
{
|
||||
$domainManager = new DomainManager(false, '');
|
||||
$listener = $this->makeListener($domainManager);
|
||||
|
||||
@@ -99,7 +105,8 @@ final class InterceptListenerTest extends TestCase {
|
||||
self::assertStringContainsString('name="nonce"', $content);
|
||||
}
|
||||
|
||||
public function testGeneratedNonceIsStoredInCache(): void {
|
||||
public function testGeneratedNonceIsStoredInCache(): void
|
||||
{
|
||||
$nonceCache = new ArrayAdapter();
|
||||
$domainManager = new DomainManager(false, '');
|
||||
$listener = $this->makeListener($domainManager, $nonceCache);
|
||||
@@ -119,7 +126,8 @@ final class InterceptListenerTest extends TestCase {
|
||||
self::assertTrue(count($nonceCache->getValues()) > 0);
|
||||
}
|
||||
|
||||
public function testLoginTemplateUsesPostFormWhenOnAuthSubdomain(): void {
|
||||
public function testLoginTemplateUsesPostFormWhenOnAuthSubdomain(): void
|
||||
{
|
||||
$domainManager = new DomainManager(true, 'auth.example.com');
|
||||
$listener = $this->makeListener($domainManager);
|
||||
|
||||
@@ -132,7 +140,8 @@ final class InterceptListenerTest extends TestCase {
|
||||
self::assertStringContainsString('method="post"', $content);
|
||||
}
|
||||
|
||||
public function testLoginTemplateDoesNotUsePostFormWhenNotOnAuthSubdomain(): void {
|
||||
public function testLoginTemplateDoesNotUsePostFormWhenNotOnAuthSubdomain(): void
|
||||
{
|
||||
$domainManager = new DomainManager(false, '');
|
||||
$listener = $this->makeListener($domainManager);
|
||||
|
||||
@@ -147,7 +156,8 @@ final class InterceptListenerTest extends TestCase {
|
||||
|
||||
/* ── invalid cookie pruning ───────────────────────────────────────── */
|
||||
|
||||
public function testInvalidCookieIsClearedWhenPresent(): void {
|
||||
public function testInvalidCookieIsClearedWhenPresent(): void
|
||||
{
|
||||
$domainManager = new DomainManager(false, '');
|
||||
$listener = $this->makeListener($domainManager);
|
||||
|
||||
@@ -171,7 +181,8 @@ final class InterceptListenerTest extends TestCase {
|
||||
self::assertTrue($cleared, 'Expected the invalid cookie to be cleared');
|
||||
}
|
||||
|
||||
public function testNoCookieClearingWhenNoCookiePresent(): void {
|
||||
public function testNoCookieClearingWhenNoCookiePresent(): void
|
||||
{
|
||||
$domainManager = new DomainManager(false, '');
|
||||
$listener = $this->makeListener($domainManager);
|
||||
|
||||
@@ -183,7 +194,8 @@ final class InterceptListenerTest extends TestCase {
|
||||
self::assertSame([], $response->headers->getCookies());
|
||||
}
|
||||
|
||||
public function testInvalidCookieUsesAuthCookieNameWithCentralAuth(): void {
|
||||
public function testInvalidCookieUsesAuthCookieNameWithCentralAuth(): void
|
||||
{
|
||||
$domainManager = new DomainManager(true, 'auth.example.com');
|
||||
$listener = $this->makeListener($domainManager);
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Unit\Listener;
|
||||
@@ -17,7 +18,8 @@ use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Event\RequestEvent;
|
||||
use Symfony\Component\HttpKernel\HttpKernelInterface;
|
||||
|
||||
final class LoginListenerTest extends TestCase {
|
||||
final class LoginListenerTest extends TestCase
|
||||
{
|
||||
use ListenerTestHelper;
|
||||
|
||||
private const string HEADER_NAME = 'X-Preauth';
|
||||
@@ -39,7 +41,8 @@ final class LoginListenerTest extends TestCase {
|
||||
return $listener;
|
||||
}
|
||||
|
||||
private function makeEvent(Request $request): RequestEvent {
|
||||
private function makeEvent(Request $request): RequestEvent
|
||||
{
|
||||
return new RequestEvent(
|
||||
$this->createStub(HttpKernelInterface::class),
|
||||
$request,
|
||||
@@ -48,14 +51,16 @@ final class LoginListenerTest extends TestCase {
|
||||
}
|
||||
|
||||
/** Build a base64url-encoded X-Preauth header value for a payload. */
|
||||
private function encodePayload(array $data): string {
|
||||
private function encodePayload(array $data): string
|
||||
{
|
||||
$json = json_encode($data, JSON_THROW_ON_ERROR);
|
||||
return rtrim(strtr(base64_encode($json), '+/', '-_'), '=');
|
||||
}
|
||||
|
||||
/* ── no login attempt ─────────────────────────────────────────────── */
|
||||
|
||||
public function testNoHeaderAndNoPostReturnsEarlyWithoutResponse(): void {
|
||||
public function testNoHeaderAndNoPostReturnsEarlyWithoutResponse(): void
|
||||
{
|
||||
$listener = $this->makeListener();
|
||||
|
||||
$request = Request::create('https://example.com/', 'GET');
|
||||
@@ -65,7 +70,8 @@ final class LoginListenerTest extends TestCase {
|
||||
self::assertFalse($event->hasResponse());
|
||||
}
|
||||
|
||||
public function testPostToNonAuthSubdomainReturnsEarlyWithoutResponse(): void {
|
||||
public function testPostToNonAuthSubdomainReturnsEarlyWithoutResponse(): void
|
||||
{
|
||||
// POST only counts as a login attempt when on the auth subdomain
|
||||
$domainManager = new DomainManager(true, 'auth.example.com');
|
||||
$listener = $this->makeListener(domainManager: $domainManager);
|
||||
@@ -79,7 +85,8 @@ final class LoginListenerTest extends TestCase {
|
||||
|
||||
/* ── successful login via header ──────────────────────────────────── */
|
||||
|
||||
public function testSuccessfulLoginViaHeaderSetsResponseFromManager(): void {
|
||||
public function testSuccessfulLoginViaHeaderSetsResponseFromManager(): void
|
||||
{
|
||||
$expected = new Response('hi alice', 200, ['Remote-User' => 'alice']);
|
||||
$loginManager = $this->createStub(LoginInterface::class);
|
||||
$loginManager->method('checkToken')->willReturn($expected);
|
||||
@@ -99,7 +106,8 @@ final class LoginListenerTest extends TestCase {
|
||||
self::assertSame($expected, $event->getResponse());
|
||||
}
|
||||
|
||||
public function testSuccessfulLoginViaPostToAuthSubdomain(): void {
|
||||
public function testSuccessfulLoginViaPostToAuthSubdomain(): void
|
||||
{
|
||||
$expected = new Response('hi bob', 303, ['Location' => '/']);
|
||||
$loginManager = $this->createStub(LoginInterface::class);
|
||||
$loginManager->method('checkToken')->willReturn($expected);
|
||||
@@ -120,7 +128,8 @@ final class LoginListenerTest extends TestCase {
|
||||
|
||||
/* ── failed login ─────────────────────────────────────────────────── */
|
||||
|
||||
public function testFailedLoginReturnsJsonErrorWithNewNonce(): void {
|
||||
public function testFailedLoginReturnsJsonErrorWithNewNonce(): void
|
||||
{
|
||||
$loginManager = $this->createStub(LoginInterface::class);
|
||||
$loginManager->method('checkToken')->willReturn(null);
|
||||
|
||||
@@ -148,7 +157,8 @@ final class LoginListenerTest extends TestCase {
|
||||
self::assertSame('alice', $body['username']);
|
||||
}
|
||||
|
||||
public function testFailedLoginHtmlResponseWhenJsonFalse(): void {
|
||||
public function testFailedLoginHtmlResponseWhenJsonFalse(): void
|
||||
{
|
||||
$loginManager = $this->createStub(LoginInterface::class);
|
||||
$loginManager->method('checkToken')->willReturn(null);
|
||||
|
||||
@@ -169,7 +179,8 @@ final class LoginListenerTest extends TestCase {
|
||||
self::assertStringContainsString('<form', $response->getContent());
|
||||
}
|
||||
|
||||
public function testFailedLoginOnAuthSubdomainUsesPostForm(): void {
|
||||
public function testFailedLoginOnAuthSubdomainUsesPostForm(): void
|
||||
{
|
||||
$loginManager = $this->createStub(LoginInterface::class);
|
||||
$loginManager->method('checkToken')->willReturn(null);
|
||||
|
||||
@@ -194,7 +205,8 @@ final class LoginListenerTest extends TestCase {
|
||||
|
||||
/* ── rate-limited (blocked) login ─────────────────────────────────── */
|
||||
|
||||
public function testRateLimitedLoginReturnsTeapotWhenTeapotEnabled(): void {
|
||||
public function testRateLimitedLoginReturnsTeapotWhenTeapotEnabled(): void
|
||||
{
|
||||
$loginManager = $this->createStub(LoginInterface::class);
|
||||
$loginManager->method('checkToken')->willReturn(null);
|
||||
|
||||
@@ -220,7 +232,8 @@ final class LoginListenerTest extends TestCase {
|
||||
self::assertSame('Teapot', $body['message']);
|
||||
}
|
||||
|
||||
public function testRateLimitedLoginReturnsTooManyRequestsWhenTeapotDisabled(): void {
|
||||
public function testRateLimitedLoginReturnsTooManyRequestsWhenTeapotDisabled(): void
|
||||
{
|
||||
$loginManager = $this->createStub(LoginInterface::class);
|
||||
$loginManager->method('checkToken')->willReturn(null);
|
||||
|
||||
@@ -252,7 +265,8 @@ final class LoginListenerTest extends TestCase {
|
||||
|
||||
/* ── invalid payload handling ─────────────────────────────────────── */
|
||||
|
||||
public function testInvalidHeaderPayloadStillRecordsFailureAndResponds(): void {
|
||||
public function testInvalidHeaderPayloadStillRecordsFailureAndResponds(): void
|
||||
{
|
||||
$loginManager = $this->createMock(LoginInterface::class);
|
||||
// checkToken should not be called with a null payload
|
||||
$loginManager->expects(self::never())->method('checkToken');
|
||||
@@ -272,7 +286,8 @@ final class LoginListenerTest extends TestCase {
|
||||
self::assertSame(Response::HTTP_UNAUTHORIZED, $event->getResponse()->getStatusCode());
|
||||
}
|
||||
|
||||
public function testPostWithoutRequiredFieldsDoesNotAttemptLogin(): void {
|
||||
public function testPostWithoutRequiredFieldsDoesNotAttemptLogin(): void
|
||||
{
|
||||
$loginManager = $this->createMock(LoginInterface::class);
|
||||
$loginManager->expects(self::never())->method('checkToken');
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Unit\Listener;
|
||||
@@ -13,7 +14,8 @@ use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Event\RequestEvent;
|
||||
use Symfony\Component\HttpKernel\HttpKernelInterface;
|
||||
|
||||
final class RejectListenerTest extends TestCase {
|
||||
final class RejectListenerTest extends TestCase
|
||||
{
|
||||
use ListenerTestHelper;
|
||||
|
||||
private function makeListener(
|
||||
@@ -29,7 +31,8 @@ final class RejectListenerTest extends TestCase {
|
||||
return $listener;
|
||||
}
|
||||
|
||||
private function makeEvent(Request $request): RequestEvent {
|
||||
private function makeEvent(Request $request): RequestEvent
|
||||
{
|
||||
return new RequestEvent(
|
||||
$this->createStub(HttpKernelInterface::class),
|
||||
$request,
|
||||
@@ -37,7 +40,8 @@ final class RejectListenerTest extends TestCase {
|
||||
);
|
||||
}
|
||||
|
||||
public function testBlockedRequestReturnsTeapotWhenTeapotEnabled(): void {
|
||||
public function testBlockedRequestReturnsTeapotWhenTeapotEnabled(): void
|
||||
{
|
||||
$listener = $this->makeListener(teapot: true, remainingTokens: 0);
|
||||
|
||||
$request = Request::create('/', 'GET', [], [], [], ['REMOTE_ADDR' => '1.2.3.4']);
|
||||
@@ -50,7 +54,8 @@ final class RejectListenerTest extends TestCase {
|
||||
self::assertSame('text/html', $response->headers->get('Content-Type'));
|
||||
}
|
||||
|
||||
public function testBlockedRequestReturnsTooManyRequestsWhenTeapotDisabled(): void {
|
||||
public function testBlockedRequestReturnsTooManyRequestsWhenTeapotDisabled(): void
|
||||
{
|
||||
$listener = $this->makeListener(teapot: false, remainingTokens: 0);
|
||||
|
||||
$request = Request::create('/', 'GET', [], [], [], ['REMOTE_ADDR' => '1.2.3.4']);
|
||||
@@ -63,7 +68,8 @@ final class RejectListenerTest extends TestCase {
|
||||
self::assertSame('text/html', $response->headers->get('Content-Type'));
|
||||
}
|
||||
|
||||
public function testUnblockedRequestSetsNoResponse(): void {
|
||||
public function testUnblockedRequestSetsNoResponse(): void
|
||||
{
|
||||
$listener = $this->makeListener(remainingTokens: 5);
|
||||
|
||||
$request = Request::create('/', 'GET', [], [], [], ['REMOTE_ADDR' => '1.2.3.4']);
|
||||
@@ -74,7 +80,8 @@ final class RejectListenerTest extends TestCase {
|
||||
self::assertFalse($event->hasResponse());
|
||||
}
|
||||
|
||||
public function testBlockedResponseContainsErrorTemplateContent(): void {
|
||||
public function testBlockedResponseContainsErrorTemplateContent(): void
|
||||
{
|
||||
$listener = $this->makeListener(teapot: true, remainingTokens: 0);
|
||||
|
||||
$request = Request::create('/', 'GET', [], [], [], ['REMOTE_ADDR' => '1.2.3.4']);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Unit;
|
||||
@@ -8,20 +9,24 @@ use OutOfBoundsException;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Cache\Adapter\ArrayAdapter;
|
||||
|
||||
final class MonitorCacheKeysTest extends TestCase {
|
||||
private function wrap(?ArrayAdapter $pool = null): MonitorCacheKeys {
|
||||
final class MonitorCacheKeysTest extends TestCase
|
||||
{
|
||||
private function wrap(?ArrayAdapter $pool = null): MonitorCacheKeys
|
||||
{
|
||||
$pool ??= new ArrayAdapter();
|
||||
return new MonitorCacheKeys($pool);
|
||||
}
|
||||
|
||||
public function testConstructorInitializesEmptyPool(): void {
|
||||
public function testConstructorInitializesEmptyPool(): void
|
||||
{
|
||||
$monitor = $this->wrap();
|
||||
|
||||
self::assertSame([], $monitor->getKeys());
|
||||
self::assertSame([], $monitor->getChanges());
|
||||
}
|
||||
|
||||
public function testSaveAddsKeyAndTracksChange(): void {
|
||||
public function testSaveAddsKeyAndTracksChange(): void
|
||||
{
|
||||
$monitor = $this->wrap();
|
||||
$item = $monitor->getItem('alpha');
|
||||
$item->set('value');
|
||||
@@ -31,7 +36,8 @@ final class MonitorCacheKeysTest extends TestCase {
|
||||
self::assertSame(['alpha' => MonitorCacheKeys::UPDATED], $monitor->getChanges());
|
||||
}
|
||||
|
||||
public function testSaveDeferredThenCommitAddsKey(): void {
|
||||
public function testSaveDeferredThenCommitAddsKey(): void
|
||||
{
|
||||
$monitor = $this->wrap();
|
||||
$item = $monitor->getItem('beta');
|
||||
$item->set('value');
|
||||
@@ -42,7 +48,8 @@ final class MonitorCacheKeysTest extends TestCase {
|
||||
self::assertSame(['beta' => MonitorCacheKeys::UPDATED], $monitor->getChanges());
|
||||
}
|
||||
|
||||
public function testGetItemReturnsUnderlyingItem(): void {
|
||||
public function testGetItemReturnsUnderlyingItem(): void
|
||||
{
|
||||
$monitor = $this->wrap();
|
||||
$item = $monitor->getItem('mykey');
|
||||
$item->set('data');
|
||||
@@ -53,7 +60,8 @@ final class MonitorCacheKeysTest extends TestCase {
|
||||
self::assertSame('data', $fetched->get());
|
||||
}
|
||||
|
||||
public function testGetItemsReturnsMultipleItems(): void {
|
||||
public function testGetItemsReturnsMultipleItems(): void
|
||||
{
|
||||
$monitor = $this->wrap();
|
||||
$a = $monitor->getItem('a');
|
||||
$a->set(1);
|
||||
@@ -70,7 +78,8 @@ final class MonitorCacheKeysTest extends TestCase {
|
||||
self::assertSame(['a' => 1, 'b' => 2], $keys);
|
||||
}
|
||||
|
||||
public function testHasItemReturnsTrueForExistingKey(): void {
|
||||
public function testHasItemReturnsTrueForExistingKey(): void
|
||||
{
|
||||
$monitor = $this->wrap();
|
||||
$item = $monitor->getItem('exists');
|
||||
$item->set('v');
|
||||
@@ -80,7 +89,8 @@ final class MonitorCacheKeysTest extends TestCase {
|
||||
self::assertFalse($monitor->hasItem('missing'));
|
||||
}
|
||||
|
||||
public function testDeleteItemRemovesKeyAndTracksRemoval(): void {
|
||||
public function testDeleteItemRemovesKeyAndTracksRemoval(): void
|
||||
{
|
||||
$monitor = $this->wrap();
|
||||
$item = $monitor->getItem('doomed');
|
||||
$item->set('v');
|
||||
@@ -93,7 +103,8 @@ final class MonitorCacheKeysTest extends TestCase {
|
||||
self::assertFalse($monitor->hasItem('doomed'));
|
||||
}
|
||||
|
||||
public function testDeleteItemOnMissingKeyIsNoop(): void {
|
||||
public function testDeleteItemOnMissingKeyIsNoop(): void
|
||||
{
|
||||
$monitor = $this->wrap();
|
||||
|
||||
$result = $monitor->deleteItem('nonexistent');
|
||||
@@ -102,7 +113,8 @@ final class MonitorCacheKeysTest extends TestCase {
|
||||
self::assertSame([], $monitor->getKeys());
|
||||
}
|
||||
|
||||
public function testDeleteItemsRemovesMultipleKeys(): void {
|
||||
public function testDeleteItemsRemovesMultipleKeys(): void
|
||||
{
|
||||
$monitor = $this->wrap();
|
||||
foreach (['x', 'y', 'z'] as $key) {
|
||||
$item = $monitor->getItem($key);
|
||||
@@ -118,7 +130,8 @@ final class MonitorCacheKeysTest extends TestCase {
|
||||
self::assertSame(MonitorCacheKeys::REMOVED, $changes['y']);
|
||||
}
|
||||
|
||||
public function testDeleteItemsWithMissingKeysStillReturnsTrue(): void {
|
||||
public function testDeleteItemsWithMissingKeysStillReturnsTrue(): void
|
||||
{
|
||||
$monitor = $this->wrap();
|
||||
|
||||
$result = $monitor->deleteItems(['ghost1', 'ghost2']);
|
||||
@@ -126,7 +139,8 @@ final class MonitorCacheKeysTest extends TestCase {
|
||||
self::assertTrue($result);
|
||||
}
|
||||
|
||||
public function testClearWipesPoolWhenNotEmpty(): void {
|
||||
public function testClearWipesPoolWhenNotEmpty(): void
|
||||
{
|
||||
$monitor = $this->wrap();
|
||||
$item = $monitor->getItem('keep');
|
||||
$item->set('v');
|
||||
@@ -138,7 +152,8 @@ final class MonitorCacheKeysTest extends TestCase {
|
||||
self::assertSame([], $monitor->getKeys());
|
||||
}
|
||||
|
||||
public function testClearIsNoopWhenEmpty(): void {
|
||||
public function testClearIsNoopWhenEmpty(): void
|
||||
{
|
||||
$monitor = $this->wrap();
|
||||
|
||||
$result = $monitor->clear();
|
||||
@@ -146,7 +161,8 @@ final class MonitorCacheKeysTest extends TestCase {
|
||||
self::assertTrue($result);
|
||||
}
|
||||
|
||||
public function testMarkCleanResetsChangeList(): void {
|
||||
public function testMarkCleanResetsChangeList(): void
|
||||
{
|
||||
$monitor = $this->wrap();
|
||||
$item = $monitor->getItem('temp');
|
||||
$item->set('v');
|
||||
@@ -160,13 +176,15 @@ final class MonitorCacheKeysTest extends TestCase {
|
||||
self::assertSame(['temp'], $monitor->getKeys());
|
||||
}
|
||||
|
||||
public function testCommitPassesThrough(): void {
|
||||
public function testCommitPassesThrough(): void
|
||||
{
|
||||
$monitor = $this->wrap();
|
||||
|
||||
self::assertTrue($monitor->commit());
|
||||
}
|
||||
|
||||
public function testSaveKeyListThrowsOutOfBoundsException(): void {
|
||||
public function testSaveKeyListThrowsOutOfBoundsException(): void
|
||||
{
|
||||
$monitor = $this->wrap();
|
||||
$item = $monitor->getItem('__key_list');
|
||||
|
||||
@@ -174,7 +192,8 @@ final class MonitorCacheKeysTest extends TestCase {
|
||||
$monitor->save($item);
|
||||
}
|
||||
|
||||
public function testSaveChangeListThrowsOutOfBoundsException(): void {
|
||||
public function testSaveChangeListThrowsOutOfBoundsException(): void
|
||||
{
|
||||
$monitor = $this->wrap();
|
||||
$item = $monitor->getItem('__chg_list');
|
||||
|
||||
@@ -182,35 +201,40 @@ final class MonitorCacheKeysTest extends TestCase {
|
||||
$monitor->save($item);
|
||||
}
|
||||
|
||||
public function testDeleteKeyListThrowsOutOfBoundsException(): void {
|
||||
public function testDeleteKeyListThrowsOutOfBoundsException(): void
|
||||
{
|
||||
$monitor = $this->wrap();
|
||||
|
||||
$this->expectException(OutOfBoundsException::class);
|
||||
$monitor->deleteItem('__key_list');
|
||||
}
|
||||
|
||||
public function testDeleteChangeListThrowsOutOfBoundsException(): void {
|
||||
public function testDeleteChangeListThrowsOutOfBoundsException(): void
|
||||
{
|
||||
$monitor = $this->wrap();
|
||||
|
||||
$this->expectException(OutOfBoundsException::class);
|
||||
$monitor->deleteItem('__chg_list');
|
||||
}
|
||||
|
||||
public function testDeleteItemsWithKeyListThrowsOutOfBoundsException(): void {
|
||||
public function testDeleteItemsWithKeyListThrowsOutOfBoundsException(): void
|
||||
{
|
||||
$monitor = $this->wrap();
|
||||
|
||||
$this->expectException(OutOfBoundsException::class);
|
||||
$monitor->deleteItems(['safe', '__key_list']);
|
||||
}
|
||||
|
||||
public function testDeleteItemsWithChangeListThrowsOutOfBoundsException(): void {
|
||||
public function testDeleteItemsWithChangeListThrowsOutOfBoundsException(): void
|
||||
{
|
||||
$monitor = $this->wrap();
|
||||
|
||||
$this->expectException(OutOfBoundsException::class);
|
||||
$monitor->deleteItems(['__chg_list']);
|
||||
}
|
||||
|
||||
public function testSaveDeferredOnKeyListThrowsOutOfBoundsException(): void {
|
||||
public function testSaveDeferredOnKeyListThrowsOutOfBoundsException(): void
|
||||
{
|
||||
$monitor = $this->wrap();
|
||||
$item = $monitor->getItem('safe');
|
||||
$item->set('value');
|
||||
@@ -223,7 +247,8 @@ final class MonitorCacheKeysTest extends TestCase {
|
||||
$monitor->saveDeferred($keyListItem);
|
||||
}
|
||||
|
||||
public function testSaveDeferredOnChangeListThrowsOutOfBoundsException(): void {
|
||||
public function testSaveDeferredOnChangeListThrowsOutOfBoundsException(): void
|
||||
{
|
||||
$monitor = $this->wrap();
|
||||
$changeListItem = $monitor->getItem('__chg_list');
|
||||
|
||||
@@ -231,7 +256,8 @@ final class MonitorCacheKeysTest extends TestCase {
|
||||
$monitor->saveDeferred($changeListItem);
|
||||
}
|
||||
|
||||
public function testGetKeysReturnsEmptyArrayWhenKeyListMissing(): void {
|
||||
public function testGetKeysReturnsEmptyArrayWhenKeyListMissing(): void
|
||||
{
|
||||
// If the underlying pool loses its key list, getKeys should return []
|
||||
$pool = new ArrayAdapter();
|
||||
$monitor = new MonitorCacheKeys($pool);
|
||||
@@ -249,7 +275,8 @@ final class MonitorCacheKeysTest extends TestCase {
|
||||
self::assertSame([], $monitor2->getKeys());
|
||||
}
|
||||
|
||||
public function testDeleteItemReturnsTrueForExistingKey(): void {
|
||||
public function testDeleteItemReturnsTrueForExistingKey(): void
|
||||
{
|
||||
$monitor = $this->wrap();
|
||||
$item = $monitor->getItem('to-delete');
|
||||
$item->set('value');
|
||||
@@ -259,7 +286,8 @@ final class MonitorCacheKeysTest extends TestCase {
|
||||
self::assertNotContains('to-delete', $monitor->getKeys());
|
||||
}
|
||||
|
||||
public function testDeleteItemsReturnsTrue(): void {
|
||||
public function testDeleteItemsReturnsTrue(): void
|
||||
{
|
||||
$monitor = $this->wrap();
|
||||
foreach (['a', 'b', 'c'] as $key) {
|
||||
$item = $monitor->getItem($key);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Unit;
|
||||
@@ -8,8 +9,10 @@ use App\PersistCache;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Cache\Adapter\ArrayAdapter;
|
||||
|
||||
final class PersistCacheTest extends TestCase {
|
||||
public function testBootWithEmptyStorageIsNoop(): void {
|
||||
final class PersistCacheTest extends TestCase
|
||||
{
|
||||
public function testBootWithEmptyStorageIsNoop(): void
|
||||
{
|
||||
$sessionCache = new ArrayAdapter();
|
||||
$sessionStorage = new ArrayAdapter();
|
||||
|
||||
@@ -21,7 +24,8 @@ final class PersistCacheTest extends TestCase {
|
||||
self::assertSame([], $monitor->getKeys());
|
||||
}
|
||||
|
||||
public function testBootLoadsFromStorageIntoCache(): void {
|
||||
public function testBootLoadsFromStorageIntoCache(): void
|
||||
{
|
||||
$sessionCache = new ArrayAdapter();
|
||||
$sessionStorage = new ArrayAdapter();
|
||||
|
||||
@@ -43,7 +47,8 @@ final class PersistCacheTest extends TestCase {
|
||||
self::assertSame([], $cacheMonitor->getChanges());
|
||||
}
|
||||
|
||||
public function testBootDoesNotReloadWhenCacheAlreadyWarm(): void {
|
||||
public function testBootDoesNotReloadWhenCacheAlreadyWarm(): void
|
||||
{
|
||||
$sessionCache = new ArrayAdapter();
|
||||
$sessionStorage = new ArrayAdapter();
|
||||
|
||||
@@ -68,7 +73,8 @@ final class PersistCacheTest extends TestCase {
|
||||
self::assertNotContains('cookie_new', $monitor->getKeys());
|
||||
}
|
||||
|
||||
public function testPersistWritesChangesToStorage(): void {
|
||||
public function testPersistWritesChangesToStorage(): void
|
||||
{
|
||||
$sessionCache = new ArrayAdapter();
|
||||
$sessionStorage = new ArrayAdapter();
|
||||
|
||||
@@ -89,7 +95,8 @@ final class PersistCacheTest extends TestCase {
|
||||
self::assertSame('user2', $storageMonitor->getItem('cookie_xyz')->get());
|
||||
}
|
||||
|
||||
public function testPersistHandlesRemovals(): void {
|
||||
public function testPersistHandlesRemovals(): void
|
||||
{
|
||||
$sessionCache = new ArrayAdapter();
|
||||
$sessionStorage = new ArrayAdapter();
|
||||
|
||||
@@ -114,7 +121,8 @@ final class PersistCacheTest extends TestCase {
|
||||
self::assertNotContains('cookie_to_remove', $storageMonitor->getKeys());
|
||||
}
|
||||
|
||||
public function testPersistIsNoopWhenNoChanges(): void {
|
||||
public function testPersistIsNoopWhenNoChanges(): void
|
||||
{
|
||||
$sessionCache = new ArrayAdapter();
|
||||
$sessionStorage = new ArrayAdapter();
|
||||
|
||||
@@ -126,7 +134,8 @@ final class PersistCacheTest extends TestCase {
|
||||
self::assertSame([], $storageMonitor->getKeys());
|
||||
}
|
||||
|
||||
public function testFullBootModifyPersistCycle(): void {
|
||||
public function testFullBootModifyPersistCycle(): void
|
||||
{
|
||||
$sessionCache = new ArrayAdapter();
|
||||
$sessionStorage = new ArrayAdapter();
|
||||
|
||||
@@ -151,7 +160,8 @@ final class PersistCacheTest extends TestCase {
|
||||
self::assertSame('cycled-user', $monitor->getItem('cookie_cycle')->get());
|
||||
}
|
||||
|
||||
public function testPersistHandlesMixedUpdatesAndRemovals(): void {
|
||||
public function testPersistHandlesMixedUpdatesAndRemovals(): void
|
||||
{
|
||||
$sessionCache = new ArrayAdapter();
|
||||
$sessionStorage = new ArrayAdapter();
|
||||
|
||||
@@ -184,7 +194,8 @@ final class PersistCacheTest extends TestCase {
|
||||
self::assertNotContains('cookie_remove', $storageMonitor->getKeys());
|
||||
}
|
||||
|
||||
public function testMultipleBootModifyPersistCycles(): void {
|
||||
public function testMultipleBootModifyPersistCycles(): void
|
||||
{
|
||||
$sessionCache = new ArrayAdapter();
|
||||
$sessionStorage = new ArrayAdapter();
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Unit\Service;
|
||||
@@ -9,10 +10,12 @@ use PHPUnit\Framework\TestCase;
|
||||
use Psr\Log\NullLogger;
|
||||
use Symfony\Component\Cache\Adapter\ArrayAdapter;
|
||||
|
||||
final class BackupCodeManagerTest extends TestCase {
|
||||
final class BackupCodeManagerTest extends TestCase
|
||||
{
|
||||
use TotpTestHelper;
|
||||
|
||||
private function makeManager(?ArrayAdapter $pool = null): BackupCodeManager {
|
||||
private function makeManager(?ArrayAdapter $pool = null): BackupCodeManager
|
||||
{
|
||||
$pool ??= new ArrayAdapter();
|
||||
$manager = new BackupCodeManager($pool);
|
||||
$manager->setConfig($this->makeConfig());
|
||||
@@ -20,7 +23,8 @@ final class BackupCodeManagerTest extends TestCase {
|
||||
return $manager;
|
||||
}
|
||||
|
||||
public function testGenerateReturnsRequestedCount(): void {
|
||||
public function testGenerateReturnsRequestedCount(): void
|
||||
{
|
||||
$manager = $this->makeManager();
|
||||
|
||||
$codes = $manager->generate(5);
|
||||
@@ -33,7 +37,8 @@ final class BackupCodeManagerTest extends TestCase {
|
||||
}
|
||||
}
|
||||
|
||||
public function testGenerateDefaultCount(): void {
|
||||
public function testGenerateDefaultCount(): void
|
||||
{
|
||||
$manager = $this->makeManager();
|
||||
|
||||
$codes = $manager->generate();
|
||||
@@ -41,7 +46,8 @@ final class BackupCodeManagerTest extends TestCase {
|
||||
self::assertCount(10, $codes);
|
||||
}
|
||||
|
||||
public function testGenerateZeroReturnsEmptyArray(): void {
|
||||
public function testGenerateZeroReturnsEmptyArray(): void
|
||||
{
|
||||
$manager = $this->makeManager();
|
||||
|
||||
$codes = $manager->generate(0);
|
||||
@@ -49,7 +55,8 @@ final class BackupCodeManagerTest extends TestCase {
|
||||
self::assertSame([], $codes);
|
||||
}
|
||||
|
||||
public function testGeneratedCodesAreStoredInCache(): void {
|
||||
public function testGeneratedCodesAreStoredInCache(): void
|
||||
{
|
||||
$pool = new ArrayAdapter();
|
||||
$manager = $this->makeManager($pool);
|
||||
|
||||
@@ -65,7 +72,8 @@ final class BackupCodeManagerTest extends TestCase {
|
||||
}
|
||||
}
|
||||
|
||||
public function testGeneratedCodesHaveFarFutureExpiry(): void {
|
||||
public function testGeneratedCodesHaveFarFutureExpiry(): void
|
||||
{
|
||||
$pool = new ArrayAdapter();
|
||||
$manager = $this->makeManager($pool);
|
||||
|
||||
@@ -77,7 +85,8 @@ final class BackupCodeManagerTest extends TestCase {
|
||||
self::assertGreaterThan((new \DateTimeImmutable('+10 years'))->getTimestamp(), (int) $expiry);
|
||||
}
|
||||
|
||||
public function testVerifyAndConsumeValidCode(): void {
|
||||
public function testVerifyAndConsumeValidCode(): void
|
||||
{
|
||||
$manager = $this->makeManager();
|
||||
$codes = $manager->generate(2);
|
||||
|
||||
@@ -86,7 +95,8 @@ final class BackupCodeManagerTest extends TestCase {
|
||||
self::assertTrue($manager->verifyAndConsume($code));
|
||||
}
|
||||
|
||||
public function testVerifyAndConsumeMarksCodeAsUsed(): void {
|
||||
public function testVerifyAndConsumeMarksCodeAsUsed(): void
|
||||
{
|
||||
$pool = new ArrayAdapter();
|
||||
$manager = $this->makeManager($pool);
|
||||
$codes = $manager->generate(1);
|
||||
@@ -99,13 +109,15 @@ final class BackupCodeManagerTest extends TestCase {
|
||||
self::assertFalse($manager->verifyAndConsume($code));
|
||||
}
|
||||
|
||||
public function testVerifyAndConsumeInvalidCode(): void {
|
||||
public function testVerifyAndConsumeInvalidCode(): void
|
||||
{
|
||||
$manager = $this->makeManager();
|
||||
|
||||
self::assertFalse($manager->verifyAndConsume('nonexistent_code'));
|
||||
}
|
||||
|
||||
public function testVerifyAndConsumeIsCaseInsensitive(): void {
|
||||
public function testVerifyAndConsumeIsCaseInsensitive(): void
|
||||
{
|
||||
$manager = $this->makeManager();
|
||||
$codes = $manager->generate(1);
|
||||
$code = $codes[0];
|
||||
@@ -114,7 +126,8 @@ final class BackupCodeManagerTest extends TestCase {
|
||||
self::assertTrue($manager->verifyAndConsume(strtoupper($code)));
|
||||
}
|
||||
|
||||
public function testVerifyAndConsumeStripsInvalidCharacters(): void {
|
||||
public function testVerifyAndConsumeStripsInvalidCharacters(): void
|
||||
{
|
||||
$manager = $this->makeManager();
|
||||
$codes = $manager->generate(1);
|
||||
$code = $codes[0];
|
||||
@@ -123,7 +136,8 @@ final class BackupCodeManagerTest extends TestCase {
|
||||
self::assertTrue($manager->verifyAndConsume(' ' . $code . '!!'));
|
||||
}
|
||||
|
||||
public function testExpireRemovesAllBackupCodes(): void {
|
||||
public function testExpireRemovesAllBackupCodes(): void
|
||||
{
|
||||
$pool = new ArrayAdapter();
|
||||
$manager = $this->makeManager($pool);
|
||||
$codes = $manager->generate(5);
|
||||
@@ -136,7 +150,8 @@ final class BackupCodeManagerTest extends TestCase {
|
||||
}
|
||||
}
|
||||
|
||||
public function testExpireWhenNoBackupCodesIsNoop(): void {
|
||||
public function testExpireWhenNoBackupCodesIsNoop(): void
|
||||
{
|
||||
$pool = new ArrayAdapter();
|
||||
$manager = $this->makeManager($pool);
|
||||
|
||||
@@ -147,7 +162,8 @@ final class BackupCodeManagerTest extends TestCase {
|
||||
self::assertTrue(true);
|
||||
}
|
||||
|
||||
public function testExpireRemovesOnlyBackupPrefixedKeys(): void {
|
||||
public function testExpireRemovesOnlyBackupPrefixedKeys(): void
|
||||
{
|
||||
$pool = new ArrayAdapter();
|
||||
$manager = $this->makeManager($pool);
|
||||
|
||||
@@ -169,14 +185,16 @@ final class BackupCodeManagerTest extends TestCase {
|
||||
}
|
||||
}
|
||||
|
||||
public function testVerifyAndConsumeEmptyStringReturnsFalse(): void {
|
||||
public function testVerifyAndConsumeEmptyStringReturnsFalse(): void
|
||||
{
|
||||
$manager = $this->makeManager();
|
||||
|
||||
// empty string after preg_replace becomes 'backup_' with nothing after it
|
||||
self::assertFalse($manager->verifyAndConsume(''));
|
||||
}
|
||||
|
||||
public function testVerifyAndConsumeCodeWithValueFalseReturnsFalse(): void {
|
||||
public function testVerifyAndConsumeCodeWithValueFalseReturnsFalse(): void
|
||||
{
|
||||
$pool = new ArrayAdapter();
|
||||
$manager = $this->makeManager($pool);
|
||||
$codes = $manager->generate(1);
|
||||
@@ -195,7 +213,8 @@ final class BackupCodeManagerTest extends TestCase {
|
||||
self::assertFalse($manager->verifyAndConsume($code));
|
||||
}
|
||||
|
||||
public function testGenerateProducesUniqueCodes(): void {
|
||||
public function testGenerateProducesUniqueCodes(): void
|
||||
{
|
||||
$manager = $this->makeManager();
|
||||
|
||||
$codes = $manager->generate(50);
|
||||
@@ -204,7 +223,8 @@ final class BackupCodeManagerTest extends TestCase {
|
||||
self::assertCount(50, array_unique($codes), 'All generated codes should be unique');
|
||||
}
|
||||
|
||||
public function testGenerateCodeLengthIsDigitsPlusTwo(): void {
|
||||
public function testGenerateCodeLengthIsDigitsPlusTwo(): void
|
||||
{
|
||||
$manager = $this->makeManager();
|
||||
|
||||
$codes = $manager->generate(1);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Unit\Service;
|
||||
@@ -6,44 +7,52 @@ namespace App\Tests\Unit\Service;
|
||||
use App\Service\DomainManager;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class DomainManagerTest extends TestCase {
|
||||
private function createManager(bool $subdomainRedirect, string $authSubdomain): DomainManager {
|
||||
final class DomainManagerTest extends TestCase
|
||||
{
|
||||
private function createManager(bool $subdomainRedirect, string $authSubdomain): DomainManager
|
||||
{
|
||||
return new DomainManager($subdomainRedirect, $authSubdomain);
|
||||
}
|
||||
|
||||
/* ── authBase / getAuthSubdomain ─────────────────────────────────────── */
|
||||
|
||||
public function testAuthBaseIsNullWhenSubdomainRedirectIsDisabled(): void {
|
||||
public function testAuthBaseIsNullWhenSubdomainRedirectIsDisabled(): void
|
||||
{
|
||||
$manager = $this->createManager(false, 'auth.example.com');
|
||||
self::assertNull($manager->authBase());
|
||||
self::assertNull($manager->getAuthSubdomain());
|
||||
}
|
||||
|
||||
public function testAuthBaseIsNullWhenAuthSubdomainIsEmpty(): void {
|
||||
public function testAuthBaseIsNullWhenAuthSubdomainIsEmpty(): void
|
||||
{
|
||||
$manager = $this->createManager(true, '');
|
||||
self::assertNull($manager->authBase());
|
||||
self::assertNull($manager->getAuthSubdomain());
|
||||
}
|
||||
|
||||
public function testAuthBaseExtractsSimpleDomain(): void {
|
||||
public function testAuthBaseExtractsSimpleDomain(): void
|
||||
{
|
||||
$manager = $this->createManager(true, 'auth.example.com');
|
||||
self::assertSame('example.com', $manager->authBase());
|
||||
self::assertSame('auth.example.com', $manager->getAuthSubdomain());
|
||||
}
|
||||
|
||||
public function testAuthBaseExtractsMultiPartTld(): void {
|
||||
public function testAuthBaseExtractsMultiPartTld(): void
|
||||
{
|
||||
$manager = $this->createManager(true, 'auth.example.co.uk');
|
||||
self::assertSame('example.co.uk', $manager->authBase());
|
||||
self::assertSame('auth.example.co.uk', $manager->getAuthSubdomain());
|
||||
}
|
||||
|
||||
public function testAuthBaseIsNullForLocalhostAuth(): void {
|
||||
public function testAuthBaseIsNullForLocalhostAuth(): void
|
||||
{
|
||||
$manager = $this->createManager(true, 'localhost');
|
||||
self::assertNull($manager->authBase());
|
||||
self::assertNull($manager->getAuthSubdomain());
|
||||
}
|
||||
|
||||
public function testAuthBaseIsNullForIpAuth(): void {
|
||||
public function testAuthBaseIsNullForIpAuth(): void
|
||||
{
|
||||
$manager = $this->createManager(true, '192.168.1.1');
|
||||
self::assertNull($manager->authBase());
|
||||
self::assertNull($manager->getAuthSubdomain());
|
||||
@@ -51,90 +60,105 @@ final class DomainManagerTest extends TestCase {
|
||||
|
||||
/* ── validReturn ──────────────────────────────────────────────────────── */
|
||||
|
||||
public function testValidReturnAcceptsAnyUrlWhenNoSubdomain(): void {
|
||||
public function testValidReturnAcceptsAnyUrlWhenNoSubdomain(): void
|
||||
{
|
||||
$manager = $this->createManager(false, '');
|
||||
self::assertTrue($manager->validReturn('https://evil.com/page'));
|
||||
self::assertTrue($manager->validReturn('https://example.com/ok'));
|
||||
}
|
||||
|
||||
public function testValidReturnRejectsInvalidUrl(): void {
|
||||
public function testValidReturnRejectsInvalidUrl(): void
|
||||
{
|
||||
$manager = $this->createManager(true, 'auth.example.com');
|
||||
self::assertFalse($manager->validReturn('not-a-url'));
|
||||
self::assertFalse($manager->validReturn(''));
|
||||
}
|
||||
|
||||
public function testValidReturnAcceptsSameBaseDomain(): void {
|
||||
public function testValidReturnAcceptsSameBaseDomain(): void
|
||||
{
|
||||
$manager = $this->createManager(true, 'auth.example.com');
|
||||
self::assertTrue($manager->validReturn('https://app.example.com/dashboard'));
|
||||
self::assertTrue($manager->validReturn('https://example.com/'));
|
||||
}
|
||||
|
||||
public function testValidReturnRejectsDifferentBaseDomain(): void {
|
||||
public function testValidReturnRejectsDifferentBaseDomain(): void
|
||||
{
|
||||
$manager = $this->createManager(true, 'auth.example.com');
|
||||
self::assertFalse($manager->validReturn('https://evil.com/phish'));
|
||||
self::assertFalse($manager->validReturn('https://other-example.com/'));
|
||||
}
|
||||
|
||||
public function testValidReturnHandlesCoUkTld(): void {
|
||||
public function testValidReturnHandlesCoUkTld(): void
|
||||
{
|
||||
$manager = $this->createManager(true, 'auth.example.co.uk');
|
||||
self::assertTrue($manager->validReturn('https://www.example.co.uk/'));
|
||||
self::assertFalse($manager->validReturn('https://example.com/'));
|
||||
}
|
||||
|
||||
public function testValidReturnRejectsUrlWithoutHost(): void {
|
||||
public function testValidReturnRejectsUrlWithoutHost(): void
|
||||
{
|
||||
$manager = $this->createManager(true, 'auth.example.com');
|
||||
self::assertFalse($manager->validReturn('mailto:test@example.com'));
|
||||
}
|
||||
|
||||
/* ── matchesAuth ──────────────────────────────────────────────────────── */
|
||||
|
||||
public function testMatchesAuthIsFalseWhenSubdomainRedirectDisabled(): void {
|
||||
public function testMatchesAuthIsFalseWhenSubdomainRedirectDisabled(): void
|
||||
{
|
||||
$manager = $this->createManager(false, 'auth.example.com');
|
||||
self::assertFalse($manager->matchesAuth('example.com'));
|
||||
self::assertFalse($manager->matchesAuth('app.example.com'));
|
||||
}
|
||||
|
||||
public function testMatchesAuthIsFalseWhenAuthSubdomainIsEmpty(): void {
|
||||
public function testMatchesAuthIsFalseWhenAuthSubdomainIsEmpty(): void
|
||||
{
|
||||
$manager = $this->createManager(true, '');
|
||||
self::assertFalse($manager->matchesAuth('example.com'));
|
||||
}
|
||||
|
||||
public function testMatchesAuthMatchesSameBaseDomain(): void {
|
||||
public function testMatchesAuthMatchesSameBaseDomain(): void
|
||||
{
|
||||
$manager = $this->createManager(true, 'auth.example.com');
|
||||
self::assertTrue($manager->matchesAuth('example.com'));
|
||||
self::assertTrue($manager->matchesAuth('app.example.com'));
|
||||
}
|
||||
|
||||
public function testMatchesAuthRejectsDifferentBaseDomain(): void {
|
||||
public function testMatchesAuthRejectsDifferentBaseDomain(): void
|
||||
{
|
||||
$manager = $this->createManager(true, 'auth.example.com');
|
||||
self::assertFalse($manager->matchesAuth('evil.com'));
|
||||
self::assertFalse($manager->matchesAuth('example.org'));
|
||||
}
|
||||
|
||||
public function testMatchesAuthHandlesMultiPartTld(): void {
|
||||
public function testMatchesAuthHandlesMultiPartTld(): void
|
||||
{
|
||||
$manager = $this->createManager(true, 'auth.example.co.uk');
|
||||
self::assertTrue($manager->matchesAuth('www.example.co.uk'));
|
||||
self::assertFalse($manager->matchesAuth('example.com'));
|
||||
}
|
||||
|
||||
public function testMatchesAuthRejectsIpHost(): void {
|
||||
public function testMatchesAuthRejectsIpHost(): void
|
||||
{
|
||||
$manager = $this->createManager(true, 'auth.example.com');
|
||||
self::assertFalse($manager->matchesAuth('192.168.1.1'));
|
||||
}
|
||||
|
||||
public function testMatchesAuthRejectsLocalhost(): void {
|
||||
public function testMatchesAuthRejectsLocalhost(): void
|
||||
{
|
||||
$manager = $this->createManager(true, 'auth.example.com');
|
||||
self::assertFalse($manager->matchesAuth('localhost'));
|
||||
}
|
||||
|
||||
/* ── baseDomain edge cases via matchesAuth ────────────────────────────── */
|
||||
|
||||
public function testMatchesAuthWithDeepSubdomain(): void {
|
||||
public function testMatchesAuthWithDeepSubdomain(): void
|
||||
{
|
||||
$manager = $this->createManager(true, 'auth.example.com');
|
||||
self::assertTrue($manager->matchesAuth('a.b.c.example.com'));
|
||||
}
|
||||
|
||||
public function testMatchesAuthWithTwoPartDomain(): void {
|
||||
public function testMatchesAuthWithTwoPartDomain(): void
|
||||
{
|
||||
/* for a 2-part auth subdomain, the baseDomain retains both parts */
|
||||
$manager = $this->createManager(true, 'auth.local');
|
||||
self::assertSame('auth.local', $manager->authBase());
|
||||
@@ -145,7 +169,8 @@ final class DomainManagerTest extends TestCase {
|
||||
|
||||
/* ── TLD table coverage ──────────────────────────────────────────────── */
|
||||
|
||||
public function testMatchesAuthWithComAuTld(): void {
|
||||
public function testMatchesAuthWithComAuTld(): void
|
||||
{
|
||||
// com.au is NOT in the TLD table (table has au? no, it doesn't),
|
||||
// so it's treated as a standard 2-part TLD: base = com.au
|
||||
$manager = $this->createManager(true, 'auth.example.com.au');
|
||||
@@ -154,7 +179,8 @@ final class DomainManagerTest extends TestCase {
|
||||
self::assertFalse($manager->matchesAuth('example.com'));
|
||||
}
|
||||
|
||||
public function testMatchesAuthWithCoJpTld(): void {
|
||||
public function testMatchesAuthWithCoJpTld(): void
|
||||
{
|
||||
// co.jp is NOT in the TLD table (table has jpn under com, not jp under co)
|
||||
// so base = co.jp
|
||||
$manager = $this->createManager(true, 'auth.example.co.jp');
|
||||
@@ -162,7 +188,8 @@ final class DomainManagerTest extends TestCase {
|
||||
self::assertTrue($manager->matchesAuth('www.example.co.jp'));
|
||||
}
|
||||
|
||||
public function testMatchesAuthWithComBrTld(): void {
|
||||
public function testMatchesAuthWithComBrTld(): void
|
||||
{
|
||||
// com.br: TLD table has com => [br], meaning *.br.com is multi-part
|
||||
// but com.br has last=br, TLD['br'] doesn't exist, so base = com.br
|
||||
$manager = $this->createManager(true, 'auth.example.com.br');
|
||||
@@ -170,35 +197,40 @@ final class DomainManagerTest extends TestCase {
|
||||
self::assertTrue($manager->matchesAuth('app.example.com.br'));
|
||||
}
|
||||
|
||||
public function testMatchesAuthWithCoNzTld(): void {
|
||||
public function testMatchesAuthWithCoNzTld(): void
|
||||
{
|
||||
// co.nz is NOT in the TLD table (nz => [co,net,org], so *.co.nz IS multi-part)
|
||||
$manager = $this->createManager(true, 'auth.example.co.nz');
|
||||
self::assertSame('example.co.nz', $manager->authBase());
|
||||
self::assertTrue($manager->matchesAuth('sub.example.co.nz'));
|
||||
}
|
||||
|
||||
public function testMatchesAuthWithComMxTld(): void {
|
||||
public function testMatchesAuthWithComMxTld(): void
|
||||
{
|
||||
// com.mx is NOT in the TLD table (mx => [com,net,org], so *.com.mx IS multi-part)
|
||||
$manager = $this->createManager(true, 'auth.example.com.mx');
|
||||
self::assertSame('example.com.mx', $manager->authBase());
|
||||
self::assertTrue($manager->matchesAuth('app.example.com.mx'));
|
||||
}
|
||||
|
||||
public function testMatchesAuthWithCoInTld(): void {
|
||||
public function testMatchesAuthWithCoInTld(): void
|
||||
{
|
||||
// co.in: in => [co,...], so *.co.in IS multi-part
|
||||
$manager = $this->createManager(true, 'auth.example.co.in');
|
||||
self::assertSame('example.co.in', $manager->authBase());
|
||||
self::assertTrue($manager->matchesAuth('app.example.co.in'));
|
||||
}
|
||||
|
||||
public function testMatchesAuthWithBrComTld(): void {
|
||||
public function testMatchesAuthWithBrComTld(): void
|
||||
{
|
||||
// br.com: TLD table has com => [br], so *.br.com IS multi-part
|
||||
$manager = $this->createManager(true, 'auth.example.br.com');
|
||||
self::assertSame('example.br.com', $manager->authBase());
|
||||
self::assertTrue($manager->matchesAuth('app.example.br.com'));
|
||||
}
|
||||
|
||||
public function testSimpleTldNotTreatedAsMultiPart(): void {
|
||||
public function testSimpleTldNotTreatedAsMultiPart(): void
|
||||
{
|
||||
// example.com is a standard 2-part domain, not multi-part
|
||||
$manager = $this->createManager(true, 'auth.example.com');
|
||||
self::assertSame('example.com', $manager->authBase());
|
||||
@@ -208,7 +240,8 @@ final class DomainManagerTest extends TestCase {
|
||||
|
||||
/* ── baseDomain edge cases ───────────────────────────────────────────── */
|
||||
|
||||
public function testMatchesAuthWithSingleLabelHost(): void {
|
||||
public function testMatchesAuthWithSingleLabelHost(): void
|
||||
{
|
||||
// a single-label domain (not localhost, not IP) has baseLength 1
|
||||
// so 'myhost' has baseDomain 'myhost', while 'auth.local' has base 'auth.local'
|
||||
// they won't match unless the auth subdomain itself is single-label
|
||||
@@ -219,22 +252,26 @@ final class DomainManagerTest extends TestCase {
|
||||
self::assertTrue($manager->matchesAuth('app.auth.local'));
|
||||
}
|
||||
|
||||
public function testMatchesAuthWithEmptyStringHost(): void {
|
||||
public function testMatchesAuthWithEmptyStringHost(): void
|
||||
{
|
||||
$manager = $this->createManager(true, 'auth.example.com');
|
||||
self::assertFalse($manager->matchesAuth(''));
|
||||
}
|
||||
|
||||
public function testValidReturnAcceptsUrlWithPort(): void {
|
||||
public function testValidReturnAcceptsUrlWithPort(): void
|
||||
{
|
||||
$manager = $this->createManager(true, 'auth.example.com');
|
||||
self::assertTrue($manager->validReturn('https://example.com:8080/path'));
|
||||
}
|
||||
|
||||
public function testValidReturnAcceptsUrlWithoutPath(): void {
|
||||
public function testValidReturnAcceptsUrlWithoutPath(): void
|
||||
{
|
||||
$manager = $this->createManager(true, 'auth.example.com');
|
||||
self::assertTrue($manager->validReturn('https://example.com'));
|
||||
}
|
||||
|
||||
public function testValidReturnRejectsDifferentDomainWithPort(): void {
|
||||
public function testValidReturnRejectsDifferentDomainWithPort(): void
|
||||
{
|
||||
$manager = $this->createManager(true, 'auth.example.com');
|
||||
self::assertFalse($manager->validReturn('https://evil.com:8080/path'));
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Unit\Service;
|
||||
@@ -18,7 +19,8 @@ use Symfony\Component\Cache\Adapter\ArrayAdapter;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
|
||||
final class LoginManagerTest extends TestCase {
|
||||
final class LoginManagerTest extends TestCase
|
||||
{
|
||||
use TotpTestHelper;
|
||||
use StringTrait;
|
||||
|
||||
@@ -62,7 +64,8 @@ final class LoginManagerTest extends TestCase {
|
||||
}
|
||||
|
||||
/** Inject a nonce directly into the manager's nonce cache. */
|
||||
private function insertNonce(LoginManager $manager, string $nonce): string {
|
||||
private function insertNonce(LoginManager $manager, string $nonce): string
|
||||
{
|
||||
$reflection = new \ReflectionProperty(LoginManager::class, 'nonceCache');
|
||||
$nonceCache = $reflection->getValue($manager);
|
||||
|
||||
@@ -74,7 +77,8 @@ final class LoginManagerTest extends TestCase {
|
||||
return $nonce;
|
||||
}
|
||||
|
||||
public function testCheckTokenReturnsNullForInvalidTotp(): void {
|
||||
public function testCheckTokenReturnsNullForInvalidTotp(): void
|
||||
{
|
||||
$manager = $this->makeLoginManager();
|
||||
$payload = $this->makePayloadWithNonce($manager, token: 'wrong-code');
|
||||
|
||||
@@ -85,7 +89,8 @@ final class LoginManagerTest extends TestCase {
|
||||
self::assertNull($manager->checkToken($payload, $request));
|
||||
}
|
||||
|
||||
public function testCheckTokenReturnsNullForSpentNonce(): void {
|
||||
public function testCheckTokenReturnsNullForSpentNonce(): void
|
||||
{
|
||||
$manager = $this->makeLoginManager();
|
||||
$payload = $this->makePayloadWithNonce($manager);
|
||||
|
||||
@@ -103,7 +108,8 @@ final class LoginManagerTest extends TestCase {
|
||||
self::assertNull($manager->checkToken($payload, $request));
|
||||
}
|
||||
|
||||
public function testCheckTokenReturnsNullForMissingNonce(): void {
|
||||
public function testCheckTokenReturnsNullForMissingNonce(): void
|
||||
{
|
||||
$manager = $this->makeLoginManager();
|
||||
|
||||
$this->backupCodeManager->method('verifyAndConsume')->willReturn(false);
|
||||
@@ -120,7 +126,8 @@ final class LoginManagerTest extends TestCase {
|
||||
self::assertNull($manager->checkToken($payload, $request));
|
||||
}
|
||||
|
||||
public function testSuccessfulTotpLoginWithCookieScopeReturnsRedirect(): void {
|
||||
public function testSuccessfulTotpLoginWithCookieScopeReturnsRedirect(): void
|
||||
{
|
||||
$manager = $this->makeLoginManager();
|
||||
$payload = $this->makePayloadWithNonce($manager, scope: Scope::Cookie);
|
||||
|
||||
@@ -136,7 +143,8 @@ final class LoginManagerTest extends TestCase {
|
||||
self::assertTrue($response->headers->has('Set-Cookie'));
|
||||
}
|
||||
|
||||
public function testSuccessfulLoginWithNoneScopeReturnsPlainResponse(): void {
|
||||
public function testSuccessfulLoginWithNoneScopeReturnsPlainResponse(): void
|
||||
{
|
||||
$manager = $this->makeLoginManager();
|
||||
$payload = $this->makePayloadWithNonce($manager, scope: Scope::None);
|
||||
|
||||
@@ -154,7 +162,8 @@ final class LoginManagerTest extends TestCase {
|
||||
self::assertFalse($response->headers->has('Location'));
|
||||
}
|
||||
|
||||
public function testSuccessfulLoginSetsRemoteUserHeader(): void {
|
||||
public function testSuccessfulLoginSetsRemoteUserHeader(): void
|
||||
{
|
||||
$manager = $this->makeLoginManager();
|
||||
$payload = $this->makePayloadWithNonce($manager, id: 'alice', scope: Scope::None);
|
||||
|
||||
@@ -168,7 +177,8 @@ final class LoginManagerTest extends TestCase {
|
||||
self::assertSame('alice', $response->headers->get('Remote-User'));
|
||||
}
|
||||
|
||||
public function testSuccessfulLoginJsonResponse(): void {
|
||||
public function testSuccessfulLoginJsonResponse(): void
|
||||
{
|
||||
$manager = $this->makeLoginManager();
|
||||
$payload = $this->makePayloadWithNonce($manager, scope: Scope::Cookie, token: null);
|
||||
$payload->json = true;
|
||||
@@ -185,7 +195,8 @@ final class LoginManagerTest extends TestCase {
|
||||
self::assertSame('Login successful', $body['message']);
|
||||
}
|
||||
|
||||
public function testSuccessfulLoginHtmlResponse(): void {
|
||||
public function testSuccessfulLoginHtmlResponse(): void
|
||||
{
|
||||
$manager = $this->makeLoginManager();
|
||||
$payload = $this->makePayloadWithNonce($manager, scope: Scope::Cookie);
|
||||
$payload->json = false;
|
||||
@@ -200,7 +211,8 @@ final class LoginManagerTest extends TestCase {
|
||||
self::assertSame('text/html', $response->headers->get('Content-Type'));
|
||||
}
|
||||
|
||||
public function testSuccessfulLoginWithReturnUrl(): void {
|
||||
public function testSuccessfulLoginWithReturnUrl(): void
|
||||
{
|
||||
$manager = $this->makeLoginManager();
|
||||
$payload = $this->makePayloadWithNonce($manager, scope: Scope::Cookie);
|
||||
|
||||
@@ -214,7 +226,8 @@ final class LoginManagerTest extends TestCase {
|
||||
self::assertSame('https://example.com/app', $response->headers->get('Location'));
|
||||
}
|
||||
|
||||
public function testSuccessfulLoginWithInvalidReturnFallsBackToPath(): void {
|
||||
public function testSuccessfulLoginWithInvalidReturnFallsBackToPath(): void
|
||||
{
|
||||
$manager = $this->makeLoginManager();
|
||||
$payload = $this->makePayloadWithNonce($manager, scope: Scope::Cookie);
|
||||
|
||||
@@ -229,7 +242,8 @@ final class LoginManagerTest extends TestCase {
|
||||
self::assertStringStartsWith('/login', $location);
|
||||
}
|
||||
|
||||
public function testIpScopeDowngradesToCookieWhenIpAccessDisabled(): void {
|
||||
public function testIpScopeDowngradesToCookieWhenIpAccessDisabled(): void
|
||||
{
|
||||
$manager = $this->makeLoginManager(ipTtl: 0);
|
||||
$payload = $this->makePayloadWithNonce($manager, scope: Scope::Ip);
|
||||
|
||||
@@ -244,7 +258,8 @@ final class LoginManagerTest extends TestCase {
|
||||
self::assertTrue($response->headers->has('Set-Cookie'));
|
||||
}
|
||||
|
||||
public function testIpScopeWhenEnabledSetsIpSession(): void {
|
||||
public function testIpScopeWhenEnabledSetsIpSession(): void
|
||||
{
|
||||
$manager = $this->makeLoginManager(ipTtl: 1800);
|
||||
$payload = $this->makePayloadWithNonce($manager, scope: Scope::Ip);
|
||||
|
||||
@@ -264,7 +279,8 @@ final class LoginManagerTest extends TestCase {
|
||||
self::assertTrue($sessionCache->hasItem('ip_1.2.3.4'));
|
||||
}
|
||||
|
||||
public function testBackupCodeAuthentication(): void {
|
||||
public function testBackupCodeAuthentication(): void
|
||||
{
|
||||
$manager = $this->makeLoginManager();
|
||||
$payload = $this->makePayloadWithNonce($manager, token: 'backup-code-123');
|
||||
|
||||
@@ -278,7 +294,8 @@ final class LoginManagerTest extends TestCase {
|
||||
self::assertSame(303, $response->getStatusCode());
|
||||
}
|
||||
|
||||
public function testNonceIsConsumedAfterSuccessfulLogin(): void {
|
||||
public function testNonceIsConsumedAfterSuccessfulLogin(): void
|
||||
{
|
||||
$manager = $this->makeLoginManager();
|
||||
$payload = $this->makePayloadWithNonce($manager);
|
||||
|
||||
@@ -296,7 +313,8 @@ final class LoginManagerTest extends TestCase {
|
||||
self::assertFalse($nonceItem->get());
|
||||
}
|
||||
|
||||
public function testUlidCollisionThrowsHttpException(): void {
|
||||
public function testUlidCollisionThrowsHttpException(): void
|
||||
{
|
||||
// Use a stub pool where every cookie_ key is already a hit (collision)
|
||||
$pool = $this->createStub(CacheItemPoolInterface::class);
|
||||
$item = $this->createStub(CacheItemInterface::class);
|
||||
@@ -358,7 +376,8 @@ final class LoginManagerTest extends TestCase {
|
||||
$manager->checkToken($payload, $request);
|
||||
}
|
||||
|
||||
public function testCookieScopeWithCentralAuthSetsDomainOnMatchingHost(): void {
|
||||
public function testCookieScopeWithCentralAuthSetsDomainOnMatchingHost(): void
|
||||
{
|
||||
$manager = $this->makeLoginManager(
|
||||
subdomainRedirect: true,
|
||||
authSubdomain: 'auth.example.com',
|
||||
@@ -381,7 +400,8 @@ final class LoginManagerTest extends TestCase {
|
||||
self::assertSame('__Http-Domain-Preauth', $cookies[0]->getName());
|
||||
}
|
||||
|
||||
public function testCookieScopeWithCentralAuthOnNonMatchingHostUsesNullDomain(): void {
|
||||
public function testCookieScopeWithCentralAuthOnNonMatchingHostUsesNullDomain(): void
|
||||
{
|
||||
$manager = $this->makeLoginManager(
|
||||
subdomainRedirect: true,
|
||||
authSubdomain: 'auth.example.com',
|
||||
@@ -404,7 +424,8 @@ final class LoginManagerTest extends TestCase {
|
||||
self::assertSame('__Http-Domain-Preauth', $cookies[0]->getName());
|
||||
}
|
||||
|
||||
public function testCheckTokenWithEmptyReturnParameterFallsBackToPath(): void {
|
||||
public function testCheckTokenWithEmptyReturnParameterFallsBackToPath(): void
|
||||
{
|
||||
$manager = $this->makeLoginManager();
|
||||
$payload = $this->makePayloadWithNonce($manager, scope: Scope::Cookie);
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Unit\Trait;
|
||||
@@ -6,18 +7,22 @@ namespace App\Tests\Unit\Trait;
|
||||
use App\Trait\CookieNameTrait;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class CookieNameTraitTest extends TestCase {
|
||||
final class CookieNameTraitTest extends TestCase
|
||||
{
|
||||
use CookieNameTrait;
|
||||
|
||||
public function testCookieName(): void {
|
||||
public function testCookieName(): void
|
||||
{
|
||||
self::assertSame('__Host-Http-Preauth', $this->cookieName());
|
||||
}
|
||||
|
||||
public function testAuthCookieName(): void {
|
||||
public function testAuthCookieName(): void
|
||||
{
|
||||
self::assertSame('__Http-Domain-Preauth', $this->authCookieName());
|
||||
}
|
||||
|
||||
public function testHeaderName(): void {
|
||||
public function testHeaderName(): void
|
||||
{
|
||||
self::assertSame('X-Preauth', $this->headerName());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Unit\Trait;
|
||||
@@ -10,20 +11,24 @@ use OTPHP\TOTPInterface;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
|
||||
final class GetTotpTraitTest extends TestCase {
|
||||
final class GetTotpTraitTest extends TestCase
|
||||
{
|
||||
use TotpTestHelper;
|
||||
|
||||
private function makeObject(): object {
|
||||
return new class {
|
||||
private function makeObject(): object
|
||||
{
|
||||
return new class () {
|
||||
use GetTotpTrait;
|
||||
|
||||
public function publicGetTotp(): TOTPInterface {
|
||||
public function publicGetTotp(): TOTPInterface
|
||||
{
|
||||
return $this->getTotp();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public function testSetConfigSetsProperty(): void {
|
||||
public function testSetConfigSetsProperty(): void
|
||||
{
|
||||
$obj = $this->makeObject();
|
||||
$config = $this->makeConfig();
|
||||
|
||||
@@ -33,7 +38,8 @@ final class GetTotpTraitTest extends TestCase {
|
||||
self::assertSame($config, $reflection->getValue($obj));
|
||||
}
|
||||
|
||||
public function testGetTotpReturnsTotpInterface(): void {
|
||||
public function testGetTotpReturnsTotpInterface(): void
|
||||
{
|
||||
$obj = $this->makeObject();
|
||||
$obj->setConfig($this->makeConfig());
|
||||
|
||||
@@ -42,7 +48,8 @@ final class GetTotpTraitTest extends TestCase {
|
||||
self::assertInstanceOf(TOTPInterface::class, $totp);
|
||||
}
|
||||
|
||||
public function testGetTotpReturnsValidCode(): void {
|
||||
public function testGetTotpReturnsValidCode(): void
|
||||
{
|
||||
$obj = $this->makeObject();
|
||||
$obj->setConfig($this->makeConfig());
|
||||
|
||||
@@ -52,14 +59,21 @@ final class GetTotpTraitTest extends TestCase {
|
||||
self::assertSame($this->validTotpCode(), $totp->now());
|
||||
}
|
||||
|
||||
public function testGetTotpThrowsOnInvalidUri(): void {
|
||||
public function testGetTotpThrowsOnInvalidUri(): void
|
||||
{
|
||||
$obj = $this->makeObject();
|
||||
$clock = $this->frozenClock();
|
||||
$utilities = $this->createUtilities($clock);
|
||||
$config = new ConfigBag(
|
||||
$utilities, $clock,
|
||||
3600, 'not-a-valid-uri', 0, false,
|
||||
'Error', 'Teapot', 'Too Many'
|
||||
$utilities,
|
||||
$clock,
|
||||
3600,
|
||||
'not-a-valid-uri',
|
||||
0,
|
||||
false,
|
||||
'Error',
|
||||
'Teapot',
|
||||
'Too Many'
|
||||
);
|
||||
$obj->setConfig($config);
|
||||
|
||||
@@ -70,16 +84,23 @@ final class GetTotpTraitTest extends TestCase {
|
||||
$obj->publicGetTotp();
|
||||
}
|
||||
|
||||
public function testGetTotpThrowsHttpExceptionWhenNotTotpType(): void {
|
||||
public function testGetTotpThrowsHttpExceptionWhenNotTotpType(): void
|
||||
{
|
||||
// A HOTP URI loads successfully as an OTPInterface but is NOT a TOTPInterface,
|
||||
// so the instanceof check in getTotp() should throw an HttpException(500)
|
||||
$obj = $this->makeObject();
|
||||
$clock = $this->frozenClock();
|
||||
$utilities = $this->createUtilities($clock);
|
||||
$config = new ConfigBag(
|
||||
$utilities, $clock,
|
||||
3600, 'otpauth://hotp/Test-HOTP?secret=JBSWY3DPEHPK3PXP&counter=0', 0, false,
|
||||
'Error', 'Teapot', 'Too Many'
|
||||
$utilities,
|
||||
$clock,
|
||||
3600,
|
||||
'otpauth://hotp/Test-HOTP?secret=JBSWY3DPEHPK3PXP&counter=0',
|
||||
0,
|
||||
false,
|
||||
'Error',
|
||||
'Teapot',
|
||||
'Too Many'
|
||||
);
|
||||
$obj->setConfig($config);
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Unit\Trait;
|
||||
@@ -7,10 +8,12 @@ use App\Trait\HasLoggerTrait;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
final class HasLoggerTraitTest extends TestCase {
|
||||
final class HasLoggerTraitTest extends TestCase
|
||||
{
|
||||
use HasLoggerTrait;
|
||||
|
||||
public function testSetLogger(): void {
|
||||
public function testSetLogger(): void
|
||||
{
|
||||
$logger = $this->createStub(LoggerInterface::class);
|
||||
$this->setLogger($logger);
|
||||
self::assertSame($logger, $this->logger);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Unit\Trait;
|
||||
@@ -15,22 +16,27 @@ use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
* Wraps the trait in a concrete class with public proxies so the protected
|
||||
* methods can be exercised from test scope.
|
||||
*/
|
||||
final class MakeNonceTraitTest extends TestCase {
|
||||
private function makeObject(): object {
|
||||
return new class {
|
||||
final class MakeNonceTraitTest extends TestCase
|
||||
{
|
||||
private function makeObject(): object
|
||||
{
|
||||
return new class () {
|
||||
use MakeNonceTrait;
|
||||
|
||||
public function publicMakeNonce(int $retries = 3): string {
|
||||
public function publicMakeNonce(int $retries = 3): string
|
||||
{
|
||||
return $this->makeNonce($retries);
|
||||
}
|
||||
|
||||
public function publicMakeCacheKey(string $name): string {
|
||||
public function publicMakeCacheKey(string $name): string
|
||||
{
|
||||
return $this->makeCacheKey($name);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public function testMakeNonceReturnsBase64UrlString(): void {
|
||||
public function testMakeNonceReturnsBase64UrlString(): void
|
||||
{
|
||||
$obj = $this->makeObject();
|
||||
$obj->setLogger(new NullLogger());
|
||||
$obj->setNonceCache(new ArrayAdapter());
|
||||
@@ -44,7 +50,8 @@ final class MakeNonceTraitTest extends TestCase {
|
||||
self::assertMatchesRegularExpression('/^[A-Za-z0-9_-]+$/', $nonce);
|
||||
}
|
||||
|
||||
public function testMakeNonceStoresNonceInCache(): void {
|
||||
public function testMakeNonceStoresNonceInCache(): void
|
||||
{
|
||||
$pool = new ArrayAdapter();
|
||||
$obj = $this->makeObject();
|
||||
$obj->setLogger(new NullLogger());
|
||||
@@ -59,7 +66,8 @@ final class MakeNonceTraitTest extends TestCase {
|
||||
self::assertTrue($item->get());
|
||||
}
|
||||
|
||||
public function testMakeNonceSetsExpiry(): void {
|
||||
public function testMakeNonceSetsExpiry(): void
|
||||
{
|
||||
$pool = new ArrayAdapter();
|
||||
$obj = $this->makeObject();
|
||||
$obj->setLogger(new NullLogger());
|
||||
@@ -74,7 +82,8 @@ final class MakeNonceTraitTest extends TestCase {
|
||||
self::assertGreaterThan(time(), (int) $expiry);
|
||||
}
|
||||
|
||||
public function testTwoNoncesAreDifferent(): void {
|
||||
public function testTwoNoncesAreDifferent(): void
|
||||
{
|
||||
$pool = new ArrayAdapter();
|
||||
$obj = $this->makeObject();
|
||||
$obj->setLogger(new NullLogger());
|
||||
@@ -86,7 +95,8 @@ final class MakeNonceTraitTest extends TestCase {
|
||||
self::assertNotSame($nonce1, $nonce2);
|
||||
}
|
||||
|
||||
public function testMakeNonceThrowsAfterMaxRetries(): void {
|
||||
public function testMakeNonceThrowsAfterMaxRetries(): void
|
||||
{
|
||||
// Create a stub pool that always reports every key as a hit (collision)
|
||||
$pool = $this->createStub(CacheItemPoolInterface::class);
|
||||
$item = $this->createStub(CacheItemInterface::class);
|
||||
@@ -105,46 +115,93 @@ final class MakeNonceTraitTest extends TestCase {
|
||||
$obj->publicMakeNonce();
|
||||
}
|
||||
|
||||
public function testMakeNonceRetriesAndSucceedsAfterCollision(): void {
|
||||
public function testMakeNonceRetriesAndSucceedsAfterCollision(): void
|
||||
{
|
||||
// Use a spy pool that returns isHit=true on the first getItem call
|
||||
// (simulating a collision), then delegates to a real ArrayAdapter for
|
||||
// subsequent calls so the retry succeeds.
|
||||
$realPool = new ArrayAdapter();
|
||||
$collisionCount = 0;
|
||||
|
||||
$spyPool = new class($realPool, $collisionCount) implements CacheItemPoolInterface {
|
||||
$spyPool = new class ($realPool, $collisionCount) implements CacheItemPoolInterface {
|
||||
private int $hits = 0;
|
||||
public function __construct(
|
||||
private CacheItemPoolInterface $inner,
|
||||
private int &$hitCounter,
|
||||
) {}
|
||||
) {
|
||||
}
|
||||
|
||||
public function getItem(string $key): CacheItemInterface {
|
||||
public function getItem(string $key): CacheItemInterface
|
||||
{
|
||||
$item = $this->inner->getItem($key);
|
||||
// pretend the first requested key is already a hit (collision)
|
||||
if ($this->hits === 0) {
|
||||
$this->hits++;
|
||||
$this->hitCounter++;
|
||||
return new class($key) implements CacheItemInterface {
|
||||
public function __construct(private string $key) {}
|
||||
public function getKey(): string { return $this->key; }
|
||||
public function get(): mixed { return true; }
|
||||
public function isHit(): bool { return true; }
|
||||
public function set(mixed $value): static { return $this; }
|
||||
public function expiresAt(?\DateTimeInterface $expiration): static { return $this; }
|
||||
public function expiresAfter(int|\DateInterval|null $time): static { return $this; }
|
||||
return new class ($key) implements CacheItemInterface {
|
||||
public function __construct(private string $key)
|
||||
{
|
||||
}
|
||||
public function getKey(): string
|
||||
{
|
||||
return $this->key;
|
||||
}
|
||||
public function get(): mixed
|
||||
{
|
||||
return true;
|
||||
}
|
||||
public function isHit(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
public function set(mixed $value): static
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
public function expiresAt(?\DateTimeInterface $expiration): static
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
public function expiresAfter(int|\DateInterval|null $time): static
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
};
|
||||
}
|
||||
return $item;
|
||||
}
|
||||
public function getItems(array $keys = []): iterable { return $this->inner->getItems($keys); }
|
||||
public function hasItem(string $key): bool { return $this->inner->hasItem($key); }
|
||||
public function clear(): bool { return $this->inner->clear(); }
|
||||
public function deleteItem(string $key): bool { return $this->inner->deleteItem($key); }
|
||||
public function deleteItems(array $keys): bool { return $this->inner->deleteItems($keys); }
|
||||
public function save(CacheItemInterface $item): bool { return $this->inner->save($item); }
|
||||
public function saveDeferred(CacheItemInterface $item): bool { return $this->inner->saveDeferred($item); }
|
||||
public function commit(): bool { return $this->inner->commit(); }
|
||||
public function getItems(array $keys = []): iterable
|
||||
{
|
||||
return $this->inner->getItems($keys);
|
||||
}
|
||||
public function hasItem(string $key): bool
|
||||
{
|
||||
return $this->inner->hasItem($key);
|
||||
}
|
||||
public function clear(): bool
|
||||
{
|
||||
return $this->inner->clear();
|
||||
}
|
||||
public function deleteItem(string $key): bool
|
||||
{
|
||||
return $this->inner->deleteItem($key);
|
||||
}
|
||||
public function deleteItems(array $keys): bool
|
||||
{
|
||||
return $this->inner->deleteItems($keys);
|
||||
}
|
||||
public function save(CacheItemInterface $item): bool
|
||||
{
|
||||
return $this->inner->save($item);
|
||||
}
|
||||
public function saveDeferred(CacheItemInterface $item): bool
|
||||
{
|
||||
return $this->inner->saveDeferred($item);
|
||||
}
|
||||
public function commit(): bool
|
||||
{
|
||||
return $this->inner->commit();
|
||||
}
|
||||
};
|
||||
|
||||
$obj = $this->makeObject();
|
||||
@@ -158,7 +215,8 @@ final class MakeNonceTraitTest extends TestCase {
|
||||
self::assertSame(1, $collisionCount, 'Expected exactly one collision before success');
|
||||
}
|
||||
|
||||
public function testMakeNonceThrowsImmediatelyWithZeroRetries(): void {
|
||||
public function testMakeNonceThrowsImmediatelyWithZeroRetries(): void
|
||||
{
|
||||
$pool = $this->createStub(CacheItemPoolInterface::class);
|
||||
$item = $this->createStub(CacheItemInterface::class);
|
||||
$item->method('isHit')->willReturn(true);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Unit\Trait;
|
||||
@@ -6,30 +7,36 @@ namespace App\Tests\Unit\Trait;
|
||||
use App\Trait\StringTrait;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class StringTraitTest extends TestCase {
|
||||
final class StringTraitTest extends TestCase
|
||||
{
|
||||
use StringTrait;
|
||||
|
||||
public function testMakeCacheKeySanitizesInvalidChars(): void {
|
||||
public function testMakeCacheKeySanitizesInvalidChars(): void
|
||||
{
|
||||
self::assertSame('hello_world', $this->makeCacheKey('hello world'));
|
||||
self::assertSame('hello_world', $this->makeCacheKey('hello!world'));
|
||||
self::assertSame('a_b_c_d', $this->makeCacheKey('a/b@c#d'));
|
||||
}
|
||||
|
||||
public function testMakeCacheKeyPreservesValidChars(): void {
|
||||
public function testMakeCacheKeyPreservesValidChars(): void
|
||||
{
|
||||
self::assertSame('ABC_123.abc', $this->makeCacheKey('ABC_123.abc'));
|
||||
}
|
||||
|
||||
public function testMakeCacheKeyTruncatesLongNames(): void {
|
||||
public function testMakeCacheKeyTruncatesLongNames(): void
|
||||
{
|
||||
$long = str_repeat('a', 300);
|
||||
$result = $this->makeCacheKey($long);
|
||||
self::assertSame(128, mb_strlen($result));
|
||||
}
|
||||
|
||||
public function testMakeCacheKeyEmptyString(): void {
|
||||
public function testMakeCacheKeyEmptyString(): void
|
||||
{
|
||||
self::assertSame('', $this->makeCacheKey(''));
|
||||
}
|
||||
|
||||
public function testMakeCacheKeyWithOnlyInvalidChars(): void {
|
||||
public function testMakeCacheKeyWithOnlyInvalidChars(): void
|
||||
{
|
||||
// preg_replace with + collapses consecutive invalid chars into one _
|
||||
self::assertSame('_', $this->makeCacheKey('!!!'));
|
||||
self::assertSame('_', $this->makeCacheKey(' '));
|
||||
@@ -37,7 +44,8 @@ final class StringTraitTest extends TestCase {
|
||||
self::assertSame('_', $this->makeCacheKey('!@ #'));
|
||||
}
|
||||
|
||||
public function testMakeCacheKeyTruncatesToExactly128(): void {
|
||||
public function testMakeCacheKeyTruncatesToExactly128(): void
|
||||
{
|
||||
$input = str_repeat('a', 128);
|
||||
self::assertSame(128, mb_strlen($this->makeCacheKey($input)));
|
||||
self::assertSame($input, $this->makeCacheKey($input));
|
||||
@@ -46,14 +54,16 @@ final class StringTraitTest extends TestCase {
|
||||
self::assertSame(128, mb_strlen($this->makeCacheKey($input129)));
|
||||
}
|
||||
|
||||
public function testMakeCacheKeyWithMultibyteChars(): void {
|
||||
public function testMakeCacheKeyWithMultibyteChars(): void
|
||||
{
|
||||
// multibyte chars are replaced with a single underscore
|
||||
$result = $this->makeCacheKey('héllo wörld');
|
||||
// é and ö are not in [A-Za-z0-9_.] so they become _
|
||||
self::assertSame('h_llo_w_rld', $result);
|
||||
}
|
||||
|
||||
public function testMakeCacheKeyWithEmoji(): void {
|
||||
public function testMakeCacheKeyWithEmoji(): void
|
||||
{
|
||||
$result = $this->makeCacheKey('a🎉b');
|
||||
self::assertSame('a_b', $result);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Unit;
|
||||
@@ -8,14 +9,17 @@ use PHPUnit\Framework\TestCase;
|
||||
use Psr\Clock\ClockInterface;
|
||||
use Symfony\Component\Cache\Adapter\ArrayAdapter;
|
||||
|
||||
final class UtilitiesTest extends TestCase {
|
||||
private function makeUtilities(?ArrayAdapter $pool = null, ?ClockInterface $clock = null): Utilities {
|
||||
final class UtilitiesTest extends TestCase
|
||||
{
|
||||
private function makeUtilities(?ArrayAdapter $pool = null, ?ClockInterface $clock = null): Utilities
|
||||
{
|
||||
$pool ??= new ArrayAdapter();
|
||||
$clock ??= $this->createStub(ClockInterface::class);
|
||||
return new Utilities($clock, $pool);
|
||||
}
|
||||
|
||||
public function testLoadTotpReturnsCachedValueWhenPresent(): void {
|
||||
public function testLoadTotpReturnsCachedValueWhenPresent(): void
|
||||
{
|
||||
$pool = new ArrayAdapter();
|
||||
$item = $pool->getItem('totp');
|
||||
$item->set('otpauth://totp/cached?secret=ABCDEFGH');
|
||||
@@ -28,7 +32,8 @@ final class UtilitiesTest extends TestCase {
|
||||
self::assertSame('otpauth://totp/cached?secret=ABCDEFGH', $result);
|
||||
}
|
||||
|
||||
public function testLoadTotpGeneratesAndStoresWhenMissing(): void {
|
||||
public function testLoadTotpGeneratesAndStoresWhenMissing(): void
|
||||
{
|
||||
$pool = new ArrayAdapter();
|
||||
$utilities = $this->makeUtilities($pool);
|
||||
|
||||
@@ -43,7 +48,8 @@ final class UtilitiesTest extends TestCase {
|
||||
self::assertSame($result, $cached->get());
|
||||
}
|
||||
|
||||
public function testLoadTotpSetsFarFutureExpiry(): void {
|
||||
public function testLoadTotpSetsFarFutureExpiry(): void
|
||||
{
|
||||
$pool = new ArrayAdapter();
|
||||
$utilities = $this->makeUtilities($pool);
|
||||
|
||||
@@ -55,7 +61,8 @@ final class UtilitiesTest extends TestCase {
|
||||
self::assertGreaterThan((new \DateTimeImmutable('+10 years'))->getTimestamp(), (int) $expiry);
|
||||
}
|
||||
|
||||
public function testLoadTotpIsIdempotentAfterGeneration(): void {
|
||||
public function testLoadTotpIsIdempotentAfterGeneration(): void
|
||||
{
|
||||
$pool = new ArrayAdapter();
|
||||
$utilities = $this->makeUtilities($pool);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user