WIP: AI Test cleanup, fixing all the tests, next up LoginListener.

This commit is contained in:
2026-06-10 13:39:58 -04:00
parent 9c332e459f
commit 7b96f992be
+91 -71
View File
@@ -4,18 +4,19 @@ declare(strict_types=1);
namespace App\Tests\Unit\Listener;
use App\ConfigBag;
use App\Data\Payload;
use App\Listener\LoginListener;
use App\Service\DomainManager;
use App\Service\LoginManager;
use App\Service\LoginInterface;
use App\Utilities;
use OTPHP\TOTP;
use PHPUnit\Framework\MockObject\Rule\InvocationOrder;
use PHPUnit\Framework\TestCase;
use Psr\Cache\CacheItemInterface;
use Psr\Cache\CacheItemPoolInterface;
use Psr\Clock\ClockInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\RateLimiter\RateLimit;
@@ -25,13 +26,13 @@ use Twig\Environment;
final class LoginListenerTest extends TestCase {
private function createEvent(Request $request): RequestEvent {
$kernel = $this->createMock(HttpKernelInterface::class);
$kernel = $this->createStub(HttpKernelInterface::class);
return new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST);
}
private function createConfigBag(bool $teapot = false): ConfigBag {
$clock = $this->createMock(ClockInterface::class);
$cache = $this->createMock(CacheItemPoolInterface::class);
$clock = $this->createStub(ClockInterface::class);
$cache = $this->createStub(CacheItemPoolInterface::class);
$utilities = new Utilities($clock, $cache);
$totp = TOTP::generate($clock);
$totp->setLabel('Test');
@@ -49,48 +50,73 @@ final class LoginListenerTest extends TestCase {
);
}
private function createLimiter(int $remainingTokens): LimiterInterface {
private function createLimiter(InvocationOrder $expectation, int $remainingTokens): LimiterInterface {
$limit = $this->createMock(RateLimit::class);
$limit->method('getRemainingTokens')->willReturn($remainingTokens);
$limit->expects($expectation)->method('getRemainingTokens')
->willReturn($remainingTokens);
$limiter = $this->createMock(LimiterInterface::class);
$limiter->method('consume')->with(1)->willReturn($limit);
$limiter->expects($expectation)->method('consume')
->with(1)->willReturn($limit);
return $limiter;
}
/**
* @param LoginInterface|null $loginManager
* @param DomainManager|null $domainManager
* @param bool $teapot Setting teapot variable used in config
* @param bool $reject Make the LoginInterface::checkToken return null when true
* @param int $limit Number of remaining login attempt tokens
* @param bool $html Indicates if we expect login form to be produced
* @param bool $cacheUsed Indicates if we expect the nonce cache to be used
* @return LoginListener */
private function createListener(
?LoginManager $loginManager = null,
?LoginInterface $loginManager = null,
?DomainManager $domainManager = null,
?RateLimiterFactoryInterface $rateLimiter = null,
bool $teapot = false
bool $teapot = false,
bool $reject = false,
int $limit = 5,
bool $html = false,
bool $cacheUsed = true,
): LoginListener {
/* we expect the login page when request is rejected and limit not reached */
$twig = $this->createMock(Environment::class);
$twig->method('render')->with('login.html.twig', self::anything())->willReturn('<html>login</html>');
$twig->expects($html ? $this->once() : $this->never())
->method('render')->with('login.html.twig', self::anything())
->willReturn('<html>login</html>');
$rateLimiter = $rateLimiter ?? $this->createMock(RateLimiterFactoryInterface::class);
$rateLimiter->method('create')->willReturn($this->createLimiter(5));
$expectation = $reject ? $this->atLeastOnce() : $this->never();
$rateLimiter = $this->createStub(RateLimiterFactoryInterface::class);
$rateLimiter->method('create')->willReturn($this->createLimiter($expectation, $limit));
$dm = $domainManager ?? new DomainManager(false, '');
$lm = $loginManager ?? $this->createMock(LoginManager::class);
if ( ! $loginManager) {
$loginManager = $this->createMock(LoginInterface::class);
$loginManager->expects($this->once())->method('checkToken')
->willReturn($reject ? null : new Response('success'));
}
$lm = $loginManager;
$config = $this->createConfigBag($teapot);
$listener = new LoginListener($twig, $rateLimiter, $dm, $lm, $config);
$listener->setLogger($this->createMock(LoggerInterface::class));
$listener->setLogger($this->createStub(LoggerInterface::class));
$cacheExpected = $cacheUsed ? $this->atLeastOnce() : $this->never();
$cache = $this->createMock(CacheItemPoolInterface::class);
$item = $this->createMock(CacheItemInterface::class);
$item->method('isHit')->willReturn(false);
$item->method('set')->willReturnSelf();
$item->method('expiresAfter')->willReturnSelf();
$cache->method('getItem')->willReturn($item);
$cache->method('save')->willReturn(true);
$item->expects($cacheExpected)->method('isHit')->willReturn(false);
$item->expects($cacheExpected)->method('set')->willReturnSelf();
$item->expects($cacheExpected)->method('expiresAfter')->willReturnSelf();
$cache->expects($cacheExpected)->method('getItem')->willReturn($item);
$cache->expects($cacheExpected)->method('save')->willReturn(true);
$listener->setNonceCache($cache);
return $listener;
}
/** no login attempt should be quietly ignored */
public function testOnKernelRequestWithNoLoginAttempt(): void {
$listener = $this->createListener();
$listener = $this->createListener($this->createStub(LoginInterface::class), cacheUsed: false);
$request = Request::create('https://example.com/');
$event = $this->createEvent($request);
@@ -99,17 +125,17 @@ final class LoginListenerTest extends TestCase {
self::assertNull($event->getResponse());
}
/** valid login via header will return success response */
public function testOnKernelRequestWithHeaderLoginSuccess(): void {
$loginManager = $this->createMock(LoginManager::class);
$loginManager->method('checkToken')->willReturnCallback(function (Payload $payload) {
return new \Symfony\Component\HttpFoundation\Response('success');
});
$listener = $this->createListener($loginManager);
$listener = $this->createListener(cacheUsed: false);
$request = Request::create('https://example.com/');
$request->headers->set('X-Preauth', base64_encode(json_encode([
'id' => 'user1', 'token' => '123456', 'nonce' => 'abc', 'json' => true, 'scope' => 'cookie'
'id' => 'user1',
'token' => '123456',
'nonce' => 'abc',
'json' => true,
'scope' => 'cookie',
])));
$event = $this->createEvent($request);
@@ -120,12 +146,10 @@ final class LoginListenerTest extends TestCase {
self::assertSame('success', $response->getContent());
}
/** valid login via post will return success response */
public function testOnKernelRequestWithPostLoginSuccess(): void {
$loginManager = $this->createMock(LoginManager::class);
$loginManager->method('checkToken')->willReturn(new \Symfony\Component\HttpFoundation\Response('success'));
$dm = new DomainManager(true, 'auth.example.com');
$listener = $this->createListener($loginManager, $dm);
$listener = $this->createListener(domainManager: $dm, cacheUsed: false);
$request = Request::create('https://auth.example.com/', 'POST', [
'username' => 'user1',
@@ -141,15 +165,17 @@ final class LoginListenerTest extends TestCase {
self::assertSame('success', $response->getContent());
}
/** rejected login will return unauthorized */
public function testOnKernelRequestWithFailedLoginReturnsUnauthorized(): void {
$loginManager = $this->createMock(LoginManager::class);
$loginManager->method('checkToken')->willReturn(null);
$listener = $this->createListener($loginManager);
$listener = $this->createListener(reject: true);
$request = Request::create('https://example.com/');
$request->headers->set('X-Preauth', base64_encode(json_encode([
'id' => 'user1', 'token' => 'bad', 'nonce' => 'abc', 'json' => true, 'scope' => 'cookie'
'id' => 'user1',
'token' => 'bad',
'nonce' => 'abc',
'json' => true,
'scope' => 'cookie',
])));
$event = $this->createEvent($request);
@@ -160,22 +186,17 @@ final class LoginListenerTest extends TestCase {
self::assertSame(401, $response->getStatusCode());
}
/** rejected login and reached rate-limit will return too-many */
public function testOnKernelRequestWithRateLimitReached(): void {
$loginManager = $this->createMock(LoginManager::class);
$loginManager->method('checkToken')->willReturn(null);
$rateLimiter = $this->createMock(RateLimiterFactoryInterface::class);
$limit = $this->createMock(RateLimit::class);
$limit->method('getRemainingTokens')->willReturn(0);
$limiter = $this->createMock(LimiterInterface::class);
$limiter->method('consume')->with(1)->willReturn($limit);
$rateLimiter->method('create')->willReturn($limiter);
$listener = $this->createListener($loginManager, null, $rateLimiter);
$listener = $this->createListener(reject: true, limit: 0);
$request = Request::create('https://example.com/');
$request->headers->set('X-Preauth', base64_encode(json_encode([
'id' => 'user1', 'token' => 'bad', 'nonce' => 'abc', 'json' => true, 'scope' => 'cookie'
'id' => 'user1',
'token' => 'bad',
'nonce' => 'abc',
'json' => true,
'scope' => 'cookie',
])));
$event = $this->createEvent($request);
@@ -186,22 +207,17 @@ final class LoginListenerTest extends TestCase {
self::assertSame(429, $response->getStatusCode());
}
/** rejected login and reached rate-limit with teapot will return teapot */
public function testOnKernelRequestWithRateLimitReachedAndTeapot(): void {
$loginManager = $this->createMock(LoginManager::class);
$loginManager->method('checkToken')->willReturn(null);
$rateLimiter = $this->createMock(RateLimiterFactoryInterface::class);
$limit = $this->createMock(RateLimit::class);
$limit->method('getRemainingTokens')->willReturn(0);
$limiter = $this->createMock(LimiterInterface::class);
$limiter->method('consume')->with(1)->willReturn($limit);
$rateLimiter->method('create')->willReturn($limiter);
$listener = $this->createListener($loginManager, null, $rateLimiter, true);
$listener = $this->createListener(teapot: true, reject: true, limit: 0);
$request = Request::create('https://example.com/');
$request->headers->set('X-Preauth', base64_encode(json_encode([
'id' => 'user1', 'token' => 'bad', 'nonce' => 'abc', 'json' => true, 'scope' => 'cookie'
'id' => 'user1',
'token' => 'bad',
'nonce' => 'abc',
'json' => true,
'scope' => 'cookie',
])));
$event = $this->createEvent($request);
@@ -212,15 +228,17 @@ final class LoginListenerTest extends TestCase {
self::assertSame(418, $response->getStatusCode());
}
/** rejected login with json will return as json */
public function testOnKernelRequestWithJsonResponse(): void {
$loginManager = $this->createMock(LoginManager::class);
$loginManager->method('checkToken')->willReturn(null);
$listener = $this->createListener($loginManager);
$listener = $this->createListener(reject: true);
$request = Request::create('https://example.com/');
$request->headers->set('X-Preauth', base64_encode(json_encode([
'id' => 'user1', 'token' => 'bad', 'nonce' => 'abc', 'json' => true, 'scope' => 'cookie'
'id' => 'user1',
'token' => 'bad',
'nonce' => 'abc',
'json' => true,
'scope' => 'cookie',
])));
$event = $this->createEvent($request);
@@ -231,11 +249,13 @@ final class LoginListenerTest extends TestCase {
self::assertSame('application/json', $response->headers->get('Content-Type'));
}
/** rejected login via POST will return as html */
public function testOnKernelRequestWithHtmlResponse(): void {
$loginManager = $this->createMock(LoginManager::class);
$loginManager->method('checkToken')->willReturn(null);
$listener = $this->createListener($loginManager);
$listener = $this->createListener(
domainManager: new DomainManager(true, 'auth.example.com'),
reject: true,
html: true,
);
$request = Request::create('https://auth.example.com/', 'POST', [
'username' => 'user1',