commit 20591647eb67ccd9c33e6b31ffc6e30f4f7477ca Author: upstream Date: Fri Sep 11 23:11:05 2026 +0200 Upstream spiral/testing state of the touched files before 6c6aa8692d diff --git a/src/Http/FakeHttp.php b/src/Http/FakeHttp.php new file mode 100644 index 0000000..2c20cd6 --- /dev/null +++ b/src/Http/FakeHttp.php @@ -0,0 +1,464 @@ + */ + private array $addedMiddleware = []; + + /** + * @param \Closure(\Closure $closure, array $bindings): mixed $scope Scope runner + */ + public function __construct( + #[Proxy] private readonly ContainerInterface $container, + private readonly FileFactory $fileFactory, + private readonly \Closure $scope, + ) {} + + public function withActor(object $actor): self + { + $this->actor = $actor; + + return $this; + } + + public function withServerVariables(array $variables): self + { + $this->defaultServerVariables = $variables; + + return $this; + } + + public function flushServerVariables(): self + { + $this->defaultServerVariables = []; + + return $this; + } + + public function withHeaders(array $headers): self + { + $this->defaultHeaders = $headers; + + return $this; + } + + public function withHeader(string $key, $value): self + { + $this->defaultHeaders[$key] = $value; + + return $this; + } + + public function withAuthorizationToken(string $token, string $type = 'Bearer'): self + { + return $this->withHeader('Authorization', $type . ' ' . $token); + } + + public function flushHeaders(): self + { + $this->defaultHeaders = []; + + return $this; + } + + public function withCookies(array $cookies): self + { + $this->defaultCookies = $cookies; + + return $this; + } + + public function withCookie(string $name, string $value): self + { + $this->defaultCookies[$name] = $value; + + return $this; + } + + public function flushCookies(): self + { + $this->defaultCookies = []; + + return $this; + } + + public function withSession( + array $data, + string $clientSignature = 'fake-session', + int $lifetime = 3600, + ?string $id = null, + ): self { + $this->session = new FakeSession($data, $clientSignature, $lifetime, $id); + + return $this; + } + + public function flushSession(): self + { + $this->session = null; + + return $this; + } + + /** + * Prepend middleware to the pipeline. + * + * @param non-empty-string ...$middleware + */ + public function withMiddleware(string ...$middleware): self + { + foreach ($middleware as $name) { + \array_unshift($this->addedMiddleware, $name); + unset($this->bindings[$name]); + } + return $this; + } + + /** + * Remove middleware from the pipeline. + * + * @param non-empty-string ...$middleware + */ + public function withoutMiddleware(string ...$middleware): self + { + foreach ($middleware as $name) { + // Remove middleware from added middleware list + $this->addedMiddleware = \array_filter($this->addedMiddleware, static fn($m): bool => $m !== $name); + + $this->bindings[$name] = new class implements MiddlewareInterface { + public function process( + ServerRequestInterface $request, + RequestHandlerInterface $handler, + ): ResponseInterface { + return $handler->handle($request); + } + }; + } + + return $this; + } + + public function getFileFactory(): FileFactory + { + return $this->fileFactory; + } + + /** + * @note The HTTP instance will not contain all configured things like middleware, etc. + * It's just a simple HTTP instance from the Container. + */ + public function getHttp(): Http + { + return $this->container->get(Http::class); + } + + public function get(string $uri, array $query = [], array $headers = [], array $cookies = []): TestResponse + { + return $this->handleRequest( + $this->createRequest($uri, 'GET', $query, $headers, $cookies), + ); + } + + public function getJson( + string $uri, + array $query = [], + array $headers = [], + array $cookies = [], + ): TestResponse { + return $this->handleRequest( + $this->createJsonRequest($uri, 'GET', $query, $headers, $cookies), + ); + } + + public function getWithAttributes(string $uri, array $attributes, array $headers = []): TestResponse + { + $r = $this->createRequest($uri, 'GET', [], $headers, []); + foreach ($attributes as $k => $v) { + $r = $r->withAttribute($k, $v); + } + + return $this->handleRequest($r); + } + + /** + * @param array|object|StreamInterface $data + */ + public function post( + string $uri, + $data = [], + array $headers = [], + array $cookies = [], + array $files = [], + ): TestResponse { + $this->validateRequestData($data); + + $request = $this->createRequest($uri, 'POST', [], $headers, $cookies, $files); + + return $this->handleRequest( + $data instanceof StreamInterface + ? $request->withBody($data) + : $request->withParsedBody($data), + ); + } + + /** + * @param array|StreamInterface $data + */ + public function postJson( + string $uri, + $data = [], + array $headers = [], + array $cookies = [], + array $files = [], + ): TestResponse { + return $this->handleRequest( + $this->createJsonRequest($uri, 'POST', $data, $headers, $cookies, $files), + ); + } + + /** + * @param array|object|StreamInterface $data + */ + public function patch( + string $uri, + $data = [], + array $headers = [], + array $cookies = [], + array $files = [], + ): TestResponse { + $this->validateRequestData($data); + + $request = $this->createRequest($uri, 'PATCH', [], $headers, $cookies, $files); + + return $this->handleRequest( + $data instanceof StreamInterface + ? $request->withBody($data) + : $request->withParsedBody($data), + ); + } + + /** + * @param array|StreamInterface $data + */ + public function patchJson( + string $uri, + $data = [], + array $headers = [], + array $cookies = [], + array $files = [], + ): TestResponse { + return $this->handleRequest( + $this->createJsonRequest($uri, 'PATCH', $data, $headers, $cookies, $files), + ); + } + + public function put( + string $uri, + $data = [], + array $headers = [], + array $cookies = [], + array $files = [], + ): TestResponse { + $this->validateRequestData($data); + + $request = $this->createRequest($uri, 'PUT', [], $headers, $cookies, $files); + + return $this->handleRequest( + $data instanceof StreamInterface + ? $request->withBody($data) + : $request->withParsedBody($data), + ); + } + + public function putJson( + string $uri, + $data = [], + array $headers = [], + array $cookies = [], + array $files = [], + ): TestResponse { + return $this->handleRequest( + $this->createJsonRequest($uri, 'PUT', $data, $headers, $cookies, $files), + ); + } + + public function delete( + string $uri, + $data = [], + array $headers = [], + array $cookies = [], + array $files = [], + ): TestResponse { + $this->validateRequestData($data); + + $request = $this->createRequest($uri, 'DELETE', [], $headers, $cookies, $files); + + return $this->handleRequest( + $data instanceof StreamInterface + ? $request->withBody($data) + : $request->withParsedBody($data), + ); + } + + public function deleteJson( + string $uri, + $data = [], + array $headers = [], + array $cookies = [], + array $files = [], + ): TestResponse { + return $this->handleRequest( + $this->createJsonRequest($uri, 'DELETE', $data, $headers, $cookies, $files), + ); + } + + public function createJsonRequest( + string $uri, + string $method, + $data, + array $headers, + array $cookies, + array $files = [], + ): ServerRequest { + if (!\is_array($data) && !$data instanceof StreamInterface) { + throw new \InvalidArgumentException( + \sprintf('$data should be array or instance of `%s` interface.', StreamInterface::class), + ); + } + + if (!$data instanceof StreamInterface) { + $data = Stream::create(\json_encode($data)); + } + + $headers = \array_merge([ + 'Content-Length' => $data->getSize(), + 'Content-Type' => 'application/json', + 'Accept' => 'application/json', + ], $headers); + + return $this->createRequest($uri, $method, [], $headers, $cookies, $files) + ->withBody($data); + } + + /** + * @param array $files + */ + public function createRequest( + string $uri, + string $method, + array $query, + array $headers, + array $cookies, + array $files = [], + ): ServerRequest { + $cookies = \array_merge($this->defaultCookies, $cookies); + $headers = \array_merge($this->defaultHeaders, $headers); + + $request = new ServerRequest( + $method, + $uri, + $headers, + 'php://input', + '1.1', + $this->defaultServerVariables, + ); + + if ($request->getUri()->getQuery() !== '') { + $uriQuery = []; + \parse_str($request->getUri()->getQuery(), $uriQuery); + $query = \array_merge($uriQuery, $query); + } else { + $request = $request->withUri($request->getUri()->withQuery(\http_build_query($query))); + } + + return $request + ->withCookieParams($cookies) + ->withQueryParams($query) + ->withUploadedFiles($files); + } + + public function handleRequest(ServerRequestInterface $request, array $bindings = []): TestResponse + { + $bindings = \array_merge($this->bindings, $bindings); + if ($this->actor) { + $request = $request->withHeader(static::AUTH_TOKEN_HEADER_KEY, \spl_object_hash($this->actor)); + + $bindings[ActorProviderInterface::class] = new FakeActorProvider($this->actor); + $bindings[TokenStorageInterface::class] = new FakeTokenStorage(); + + $transport = new TransportRegistry(); + $transport->setTransport('testing', new HeaderTransport(static::AUTH_TOKEN_HEADER_KEY)); + $bindings[TransportRegistry::class] = $transport; + } + + if ($this->session) { + $bindings[SessionInterface::class] = $this->session; + } + + $handler = function () use ($request): ResponseInterface { + if ($this->addedMiddleware === []) { + return $this->getHttp()->handle($request); + } + + // Add middleware to the pipeline + /** @var FactoryInterface $factory */ + $factory = $this->container->get(FactoryInterface::class); + $pipeline = $factory->make(LazyPipeline::class); + return $pipeline->withMiddleware(...$this->addedMiddleware) + ->withHandler($this->getHttp()) + ->handle($request); + }; + + try { + return new TestResponse(($this->scope)($handler, $bindings)); + } finally { + try { + $this->container->get(FinalizerInterface::class)->finalize(false); + } catch (\Throwable) { + // Ignore exceptions when finalizer is out of scope + } + } + } + + protected function validateRequestData($data): void + { + if (!\is_array($data) && !\is_object($data)) { + throw new \InvalidArgumentException('$data should be an array or an object.'); + } + } +}