*/ 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(); $bindings[TransportRegistry::class] = $this->createFakeTransportRegistry(); } 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.'); } } /** * The fake token travels in the {@see self::AUTH_TOKEN_HEADER_KEY} header, so every transport the * application has configured is replaced by one that reads it. The configured names are kept because * {@see \Spiral\Auth\Middleware\AuthTransportMiddleware} resolves a transport by name. */ private function createFakeTransportRegistry(): TransportRegistry { $registry = new TransportRegistry(); $transport = new HeaderTransport(static::AUTH_TOKEN_HEADER_KEY); try { /** @var TransportRegistry $configured */ $configured = $this->container->get(TransportRegistry::class); foreach (\array_keys($configured->getTransports()) as $name) { $registry->setTransport((string) $name, $transport); } } catch (NotFoundExceptionInterface) { // Nothing to copy the names from; the fake transport is all there is. } $registry->setTransport('testing', $transport); $registry->setDefaultTransport('testing'); return $registry; } }