| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203 |
- <?php
- declare(strict_types=1);
- namespace App\Tests\Integration\Support;
- use App\App\AppFactory;
- use App\App\Container;
- use App\Auth\OidcAuthenticator;
- use GuzzleHttp\ClientInterface as GuzzleClientInterface;
- use GuzzleHttp\Handler\MockHandler;
- use GuzzleHttp\HandlerStack;
- use GuzzleHttp\Middleware;
- use Monolog\Handler\NullHandler;
- use Monolog\Handler\TestHandler;
- use Monolog\Logger;
- use PHPUnit\Framework\TestCase;
- use Psr\Container\ContainerInterface;
- use Psr\Log\LoggerInterface;
- use Slim\App;
- use Slim\Psr7\Factory\ServerRequestFactory;
- use Slim\Psr7\Factory\StreamFactory;
- /**
- * Boots the UI Slim app with a Guzzle MockHandler swapped in for the
- * api-side calls. Sessions run in CLI-fallback mode (no headers); each
- * test gets a fresh `$_SESSION` superglobal.
- *
- * The mocked responses are queued via `enqueueApiResponse(...)` before
- * the request under test fires.
- */
- abstract class AppTestCase extends TestCase
- {
- protected ContainerInterface $container;
- protected App $app;
- protected MockHandler $mock;
- /** @var list<array{request: \Psr\Http\Message\RequestInterface, response: \Psr\Http\Message\ResponseInterface, error: mixed, options: array<string, mixed>}> */
- protected array $apiHistory = [];
- private ?string $throttlePath = null;
- protected function tearDown(): void
- {
- parent::tearDown();
- if ($this->throttlePath !== null) {
- @unlink($this->throttlePath);
- @unlink($this->throttlePath . '.lock');
- $this->throttlePath = null;
- }
- }
- /**
- * @param array<string, mixed> $overrides
- */
- protected function bootApp(array $overrides = []): void
- {
- // Reset the global session bag between tests.
- $_SESSION = [];
- $this->throttlePath = sys_get_temp_dir() . '/irdb_login_throttle_test_'
- . bin2hex(random_bytes(8)) . '.json';
- $defaults = [
- 'app_env' => 'development',
- 'log_level' => \Monolog\Level::Warning,
- 'public_url' => 'http://localhost:8080',
- 'api_base_url' => 'http://api:8081',
- 'ui_service_token' => 'irdb_svc_TESTTOKEN',
- 'api_timeout_seconds' => 1.0,
- 'oidc_enabled' => false,
- 'oidc_issuer' => '',
- 'oidc_client_id' => '',
- 'oidc_client_secret' => '',
- 'oidc_redirect_uri' => '',
- 'local_admin_enabled' => true,
- 'local_admin_username' => 'admin',
- 'local_admin_password_hash' => password_hash('test1234', PASSWORD_ARGON2ID),
- 'session_idle_seconds' => 28800,
- 'session_absolute_seconds' => 86400,
- 'login_throttle_path' => $this->throttlePath,
- ];
- $settings = array_replace($defaults, $overrides);
- $this->container = Container::build($settings);
- $this->mock = new MockHandler();
- $this->apiHistory = [];
- if (method_exists($this->container, 'set')) {
- /** @var \DI\Container $c */
- $c = $this->container;
- $handler = HandlerStack::create($this->mock);
- $handler->push(Middleware::history($this->apiHistory));
- $c->set(GuzzleClientInterface::class, new \GuzzleHttp\Client(['handler' => $handler]));
- // ApiClient closure-builds from the container; refresh the
- // bound instance with our mocked Guzzle client.
- $c->set(\App\ApiClient\ApiClient::class, new \App\ApiClient\ApiClient(
- http: $c->get(GuzzleClientInterface::class),
- serviceToken: (string) $c->get('settings.ui_service_token'),
- health: $c->get(\App\ApiClient\ApiHealth::class),
- ));
- // Quiet the logger so test output stays clean.
- $nullLogger = new Logger('test');
- $nullLogger->pushHandler(new NullHandler());
- $c->set(LoggerInterface::class, $nullLogger);
- }
- $this->app = AppFactory::build($this->container);
- }
- /**
- * @param array<string, mixed> $body
- */
- protected function enqueueApiResponse(int $status, array $body, string $contentType = 'application/json'): void
- {
- $this->mock->append(new \GuzzleHttp\Psr7\Response(
- $status,
- ['Content-Type' => $contentType],
- (string) json_encode($body),
- ));
- }
- protected function enqueueApiException(\Throwable $e): void
- {
- $this->mock->append($e);
- }
- /**
- * @param array<string, string> $headers
- * @param array<string, mixed> $serverParams
- */
- protected function request(
- string $method,
- string $path,
- array $headers = [],
- ?string $body = null,
- ?string $contentType = null,
- array $serverParams = [],
- ): \Psr\Http\Message\ResponseInterface {
- $factory = new ServerRequestFactory();
- $request = $factory->createServerRequest($method, $path, $serverParams);
- foreach ($headers as $name => $value) {
- $request = $request->withHeader($name, $value);
- }
- if ($contentType !== null) {
- $request = $request->withHeader('Content-Type', $contentType);
- }
- if ($body !== null) {
- $stream = (new StreamFactory())->createStream($body);
- $request = $request->withBody($stream);
- }
- return $this->app->handle($request);
- }
- /**
- * Swap in a Monolog `TestHandler` so a test can assert what reached
- * the logger. Re-builds controllers that captured the old logger
- * (currently just `OidcController`) so they pick up the new binding.
- */
- protected function captureLogs(): TestHandler
- {
- $handler = new TestHandler();
- $logger = new Logger('test');
- $logger->pushHandler($handler);
- if (method_exists($this->container, 'set')) {
- /** @var \DI\Container $c */
- $c = $this->container;
- $c->set(LoggerInterface::class, $logger);
- $c->set(\App\Auth\OidcController::class, new \App\Auth\OidcController(
- authenticator: $c->get(OidcAuthenticator::class),
- auth: $c->get(\App\ApiClient\AuthClient::class),
- sessions: $c->get(\App\Auth\SessionManager::class),
- logger: $logger,
- enabled: (bool) $c->get('settings.oidc_enabled'),
- ));
- $this->app = AppFactory::build($c);
- }
- return $handler;
- }
- /**
- * Replace the `OidcAuthenticator` binding with a fake callback that
- * the test controls. Lets us drive the OIDC controller's success +
- * failure paths without a real IdP.
- */
- protected function bindOidcAuthenticator(OidcAuthenticator $stub): void
- {
- if (method_exists($this->container, 'set')) {
- /** @var \DI\Container $c */
- $c = $this->container;
- $c->set(OidcAuthenticator::class, $stub);
- // Re-build the OidcController so it picks up the new binding.
- $c->set(\App\Auth\OidcController::class, new \App\Auth\OidcController(
- authenticator: $stub,
- auth: $c->get(\App\ApiClient\AuthClient::class),
- sessions: $c->get(\App\Auth\SessionManager::class),
- logger: $c->get(LoggerInterface::class),
- enabled: (bool) $c->get('settings.oidc_enabled'),
- ));
- $this->app = AppFactory::build($c);
- }
- }
- }
|