}> */ 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 $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', 'ui_secret' => 'test', '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 $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 $headers * @param array $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); } /** * 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); } } }