test_helpers-1.0.0-alpha6/src/Stub/HttpClientFactoryStub.php

src/Stub/HttpClientFactoryStub.php
<?php

namespace Drupal\test_helpers\Stub;

use Drupal\Component\Serialization\Yaml;
use Drupal\Core\Http\ClientFactory;
use Drupal\test_helpers\TestHelpers;
use GuzzleHttp\Exception\BadResponseException;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Promise\FulfilledPromise;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Response;
use Psr\Http\Message\ResponseInterface;

/**
 * Helper class to construct a HTTP client for capturing and mocking responses.
 *
 * It allows to grab the responses from the real requests and store them to the
 * files in the 'store' mode.
 *
 * And in the 'mock' mode - read the responses from files without making real
 * HTTP requests.
 *
 * @see `tests/src/Unit/Stub/HttpClientFactoryStubTest.php` for the usage
 * examples.
 */
class HttpClientFactoryStub extends ClientFactory {

  /**
   * The environment variable to control the HTTP client mode - store or mock.
   *
   * @var string
   */
  const EMV_HTTP_CLIENT_MODE = 'TH_HTTP_CLIENT_MODE';

  /**
   * The mode to store all outgoing requests responses to files.
   *
   * @var string
   */
  const HTTP_CLIENT_MODE_STORE = 'store';
  /**
   * The mode to mock all outgoing requests responses from files.
   *
   * @var string
   */
  const HTTP_CLIENT_MODE_MOCK = 'mock';
  /**
   * The mode to store requests responses only if the stored one is missing.
   *
   * @var string
   */
  const HTTP_CLIENT_MODE_APPEND = 'append';

  /**
   * The HTTP handler names to find them in the stack.
   *
   * @var string
   */
  const HANDLER_NAME_CUSTOM = 'test_helpers_http_client_mock.handler_custom';
  const HANDLER_NAME_CUSTOM_RESPONSES_STACK = 'test_helpers_http_client_mock.handler_custom_responses_stack';
  const HANDLER_NAME_BEFORE_REAL_CALL = 'test_helpers_http_client_mock.handler_before_real_call';
  const HANDLER_NAME_AFTER_REAL_CALL = 'test_helpers_http_client_mock.handler_after_real_call';

  /**
   * The options keys.
   */
  const OPTION_STORE_HEADERS = 'store_headers';
  const OPTION_STORE_HEADERS_SKIP_KEYS = 'store_headers_skip';
  const OPTION_LOG_STORED_RESPONSES_USAGE_FILE = 'log_stored_responses_usage_file';
  const OPTION_CONTEXT = 'context';
  const OPTION_URI_REGEXP = 'uri_regexp';

  /**
   * A storage for used (stored and mocked) responses by the usage order.
   *
   * @var array
   */
  protected array $handledRequests = [];

  /**
   * A stack with custom responses.
   *
   * @var \GuzzleHttp\Psr7\Response[]
   */
  protected array $stubCustomResponsesStack = [];

  /**
   * The handler stack, attached to the last created HTTP client.
   *
   * Used to dynamically control the custom handler.
   *
   * @var \GuzzleHttp\HandlerStack|null
   */
  protected $stubHandlerStackLast = NULL;
  /**
   * A custom handler for HTTP requests.
   *
   * @var callable|null
   */
  protected $stubHandlerCustom = NULL;

  /**
   * The context for the stub that is used to generate the stored assets hash.
   *
   * @var string|null
   */
  protected ?string $stubContext = NULL;

  /**
   * HttpClientFactoryStub constructor.
   *
   * @param \GuzzleHttp\HandlerStack|null $stack
   *   The HTTP client stack.
   * @param string|null $requestMockMode
   *   The requests mocking mode: NULL, 'store', 'mock', 'append'.
   *   - append: makes a new request only if the stored response is missing.
   * @param string|null $responsesStorageDirectory
   *   The directory to store responses.
   * @param string|null $testName
   *   The name of the test.
   * @param string|null $options
   *   An associative array of options.
   *   Supported keys:
   *   - store_headers: (bool) Store headers of the response. Defaults to FALSE.
   *   - skip_headers: (array) A list of headers to not store. Defaults to [].
   *   - uri_regexp: (string) A regular expression for the URI to store.
   *     Defaults to ''.
   */
  public function __construct(
    ?HandlerStack $stack = NULL,
    protected ?string $requestMockMode = NULL,
    protected ?string $responsesStorageDirectory = NULL,
    protected ?string $testName = NULL,
    protected ?array $options = NULL,
  ) {
    $stack = $stack ?? HandlerStack::create();
    $this->options ??= [];
    $this->options += [
      self::OPTION_STORE_HEADERS => FALSE,
      self::OPTION_STORE_HEADERS_SKIP_KEYS => [],
      self::OPTION_LOG_STORED_RESPONSES_USAGE_FILE => NULL,
    ];
    if (array_key_exists(self::OPTION_CONTEXT, $this->options)) {
      $this->stubContext = $this->options[self::OPTION_CONTEXT];
    }
    $this->stubSetTestName($testName);
    parent::__construct($stack);
  }

  /**
   * {@inheritdoc}
   */
  public function fromOptions(array $config = []) {
    $config = $this->stubAlterConfigOptions($config);
    return parent::fromOptions($config);
  }

  /**
   * Allows altering config options before creating the client.
   *
   * @param array $config
   *   The configuration options for the HTTP client.
   *
   * @return array
   *   The altered configuration options.
   */
  protected function stubAlterConfigOptions(array $config = []): array {
    // Setting the default handler, if the custom one is not set.
    $config['handler'] ??= $this->stack;

    $this->stubHandlerStackLast = $config['handler'];

    $lastMockingResult = NULL;

    // A request handler that executes before all other handlers.
    $handlerBeforeRealCall = function (callable $handler) use (&$lastMockingResult) {
      // @todo Simplify this by not run the custom function if no needs.
      return function ($request, array $options) use ($handler, &$lastMockingResult) {
        $lastMockingResult = NULL;
        if (
          in_array($this->stubGetRequestMockMode(), [
            self::HTTP_CLIENT_MODE_MOCK,
            self::HTTP_CLIENT_MODE_APPEND,
          ])
          && $this->stubMatchRequest($request)
        ) {

          // For the append mode, we should check if the response is already
          // stored and do not produce an exception on missing stored response.
          if ($this->stubGetRequestMockMode() == self::HTTP_CLIENT_MODE_APPEND) {
            if ($this->stubHasStoredResponse($request)) {
              $response = $this->stubGetStoredResponse($request);
              $lastMockingResult = TRUE;
              return new FulfilledPromise($response);
            }
            else {
              $lastMockingResult = FALSE;
            }
          }
          else {
            $response = $this->stubGetStoredResponse($request);
            return new FulfilledPromise($response);
          }
        }
        // If the response returns non-2xx result, the Guzzle will throw an
        // exception, that stops processing next handlers.
        // So we should catch it and store the response here.
        try {
          $handlerResult = $handler($request, $options);
          return $handlerResult;
        }
        catch (BadResponseException $e) {
          if (
            in_array($this->stubGetRequestMockMode(), [
              self::HTTP_CLIENT_MODE_STORE,
              self::HTTP_CLIENT_MODE_APPEND,
            ])
            && $this->stubMatchRequest($request)
          ) {
            $failedResponse = $e->getResponse();
            $this->stubStoreResponse($failedResponse, $request);
          }
          throw $e;
        }
      };
    };

    // A request handler that executes after all other handlers.
    $handlerAfterRealCall = function (callable $handler) use (&$lastMockingResult) {
      // @todo Simplify this by not run the custom function if no needs.
      return function ($request, array $options) use ($handler, &$lastMockingResult) {
        if (
          (
            $this->stubGetRequestMockMode() == self::HTTP_CLIENT_MODE_STORE
            || $lastMockingResult === FALSE
          )
        ) {
          if ($this->stubMatchRequest($request)) {
            // Execute the real request to get the response.
            try {
              $response = $handler($request, $options)->then(
                function ($response) use ($request) {
                  $this->stubStoreResponse($response, $request);
                  return $response;
                }
              );
              return $response;
            }
            catch (BadResponseException $e) {
              // If retrieving the response failed, we should store the
              // response from the exception.
              $response = $e->getResponse();
              $this->stubStoreResponse($response, $request);
            }
          }
        }
        return $handler($request, $options);
      };
    };

    // A request handler that executes before all other handlers.
    $handlerCustomResponsesStack = function (callable $handler) {
      // @todo Simplify this by not run the custom function if no needs.
      return function ($request, array $options) use ($handler) {
        if (!empty($this->stubCustomResponsesStack)) {
          $response = array_shift($this->stubCustomResponsesStack);
          return new FulfilledPromise($response);
        }
        return $handler($request, $options);
      };
    };

    // Add custom handlers to the stack.
    // And clean up already added our handlers, if present.
    $config['handler']->remove(self::HANDLER_NAME_AFTER_REAL_CALL);
    $config['handler']->push($handlerAfterRealCall, self::HANDLER_NAME_AFTER_REAL_CALL);

    $config['handler']->remove(self::HANDLER_NAME_BEFORE_REAL_CALL);
    $config['handler']->unshift($handlerBeforeRealCall, self::HANDLER_NAME_BEFORE_REAL_CALL);

    $config['handler']->remove(self::HANDLER_NAME_CUSTOM);
    if ($this->stubHandlerCustom) {
      $config['handler']->unshift($this->stubHandlerCustom, self::HANDLER_NAME_CUSTOM);
    }

    $config['handler']->remove(self::HANDLER_NAME_CUSTOM_RESPONSES_STACK);
    $config['handler']->unshift($handlerCustomResponsesStack, self::HANDLER_NAME_CUSTOM_RESPONSES_STACK);

    return $config;
  }

  /**
   * Checks if a stored response exists for the given request.
   *
   * @param \GuzzleHttp\Psr7\Request $request
   *   The request object to check for a stored response.
   *
   * @return bool
   *   TRUE if a stored response exists, FALSE otherwise.
   */
  public function stubHasStoredResponse(Request $request): bool {
    $hash = $this->stubGetRequestHash($request);
    $file = $this->stubGetRequestFilename($hash);
    return file_exists($file);
  }

  /**
   * Gets the stored response of a request from the storage.
   *
   * @param \GuzzleHttp\Psr7\Request $request
   *   A request.
   *
   * @throws \Exception
   *   When there is no stored response found.
   *
   * @return \GuzzleHttp\Psr7\Response
   *   The stored response.
   */
  public function stubGetStoredResponse(Request $request): Response {
    $hash = $this->stubGetRequestHash($request);
    $this->stubStoreRequestHashUsage($hash);
    try {
      $response = $this->stubGetStoredResponseByHash($hash);
    }
    catch (\Exception $e) {
      throw new \Exception(
        $e->getMessage()
        . " in the \"mock\" mode. Request: "
        . $request->getMethod() . ' ' . $request->getUri()
        . " Use the '" . self::EMV_HTTP_CLIENT_MODE . "=store' environment variable to create files with stored responses."
      );
    }
    return $response;
  }

  /**
   * Get the stored response from the storage by the request hash.
   *
   * @param string $hash
   *   A request hash.
   *
   * @return \GuzzleHttp\Psr7\Response
   *   The stored response.
   */
  public function stubGetStoredResponseByHash(string $hash): Response {
    $file = $this->stubGetRequestFilename($hash);

    try {
      // The `file_get_contents` throws a warning if the file doesn't exist,
      // so we have to do an additional check to get rid of this warning.
      // @todo Remove this exception when dropping PHPUnit 9 support.
      if (!file_exists($file)) {
        throw new \Exception("Missing the stored response file for the request with hash $hash - expected to find file $file.");
      }

      $body = file_get_contents($file);
      if ($body === FALSE) {
        throw new \Exception("Can't read the stored response file for the request with hash $hash - expected to find file $file.");
      }

      // The `file_get_contents` throws a warning if the file doesn't exist,
      // so we have to do an additional check to get rid of this warning.
      // @todo Remove this exception when dropping PHPUnit 9 support.
      $fileMetadata = $this->stubGetRequestFilename($hash, metadata: TRUE);
      if (!file_exists($fileMetadata)) {
        throw new \Exception("Missing the stored response metadata file for the request with hash $hash - expected to find file $file.");
      }
      $metadata = Yaml::decode(file_get_contents($fileMetadata));
      if ($metadata == FALSE) {
        throw new \Exception("Can't read the stored response metadata file for the request with hash $hash - expected to find file $fileMetadata.");
      }

      $status = 200;
      $headers = [];
      if (isset($metadata['response'])) {
        $status = $metadata['response']['status'];
        if (
          $this->options[self::OPTION_STORE_HEADERS]
          && isset($metadata['response']['headers'])
        ) {
          $headers = $metadata['response']['headers'];
        }
      }
      if (!$this->ensureTestNameInMetadata($metadata)) {
        $this->stubSetStoredResponseMetadataByHash($metadata, $hash);
      }
      $this->stubLogResponseUsage($hash, 'read');
    }
    catch (\Exception $e) {
      $this->stubLogResponseUsage($hash, 'missing');
      throw $e;
    }

    $response = new Response(
      status: $status,
      headers: $headers,
      body: $body,
    );
    return $response;
  }

  /**
   * Get the stored response metadata from the storage by the request hash.
   *
   * @param string $hash
   *   A request hash.
   *
   * @return array
   *   The stored response metadata array.
   */
  public function stubGetStoredResponseMetadataByHash(string $hash): array {
    $fileMetadata = $this->stubGetRequestFilename($hash, metadata: TRUE);
    if (!$metadata = Yaml::decode(file_get_contents($fileMetadata))) {
      throw new \Exception("No stored metadata found for the hash \"$hash\" in the file " . $fileMetadata);
    }
    return $metadata;
  }

  /**
   * Stores the stored response metadata by the request hash.
   *
   * @param array $metadata
   *   The metadata to set.
   * @param string $hash
   *   A request hash.
   */
  public function stubSetStoredResponseMetadataByHash(array $metadata, string $hash): void {
    $fileMetadata = $this->stubGetRequestFilename($hash, metadata: TRUE);
    if (file_exists($fileMetadata)) {
      $storedContent = file_get_contents($fileMetadata);
      if ($storedContent == Yaml::encode($metadata)) {
        return;
      }
    }
    file_put_contents($fileMetadata, Yaml::encode($metadata));
  }

  /**
   * Deletes the stored response files from the storage by the request hash.
   *
   * @param string $hash
   *   A request hash.
   */
  public function stubDeleteStoredResponseByHash(string $hash): void {
    unlink($this->stubGetRequestFilename($hash));
    unlink($this->stubGetRequestFilename($hash, metadata: TRUE));
  }

  /**
   * Gets the current test name, or generates it if not set.
   *
   * @return string
   *   The current test name.
   */
  public function stubGetTestName(): string {
    return $this->testName;
  }

  /**
   * Sets the test name, autodetect the name if not provided.
   *
   * @param string|null $name
   *   The test name. If NULL - tries to autodetect it.
   */
  public function stubSetTestName(?string $name = NULL): void {
    if ($name !== NULL) {
      $this->testName = $name;
    }
    elseif ($this->stubIsPhpunitTest()) {
      // Autodetect the test name from parent callers.
      $backtrace = debug_backtrace();
      foreach ($backtrace as $item) {
        if (
          in_array($item['class'] ?? [], [
            HttpClientFactoryStub::class,
            TestHelpers::class,
          ])) {
          continue;
        }
        $this->testName = $item['class'] . '::' . $item['function'];
        break;
      }
    }
    else {
      // @todo Make auto detection of test name in functional tests.
      $this->testName = 'undefined';
    }
  }

  /**
   * Sets the context value to use for generating the stored responses hash.
   *
   * @param string|null $context
   *   The context value. Use NULL to reset the context.
   */
  public function stubSetContext(?string $context = NULL): void {
    $this->stubContext = $context;
  }

  /**
   * Gets the current context value used in generating responses hash.
   *
   * @return string|null
   *   The context value.
   */
  public function stubGetContext(): ?string {
    return $this->stubContext;
  }

  /**
   * Returns the current HTTP Requests mocking mode: none, store, mock.
   *
   * If the mode is not set explicitly, it is controllable by the
   * `TH_HTTP_CLIENT_MODE` environment variable:
   * - `TH_HTTP_CLIENT_MODE=store` enables real requests and store them.
   * - `TH_HTTP_CLIENT_MODE=mock` mock all requests from the storage.
   *
   * @return string|null
   *   The current mode:
   *   - NULL - works as default Drupal http_client
   *   - store - stores all response to the storage.
   *   - mock - mocks all requests from the storage.
   */
  public function stubGetRequestMockMode(): ?string {
    if ($this->requestMockMode === NULL) {
      switch (getenv(self::EMV_HTTP_CLIENT_MODE)) {
        case self::HTTP_CLIENT_MODE_STORE:
          return self::HTTP_CLIENT_MODE_STORE;

        case self::HTTP_CLIENT_MODE_MOCK:
          return self::HTTP_CLIENT_MODE_MOCK;

        default:
          return NULL;
      }
    }
    else {
      return $this->requestMockMode;
    }
  }

  /**
   * Sets the HTTP Requests mocking mode.
   *
   * @param mixed $mode
   *   A mode: store, mock, append or NULL to use the Drupal default mode.
   */
  public function stubSetRequestMockMode(string $mode): void {
    $this->requestMockMode = $mode;
  }

  /**
   * Stores the response for a request to the storage.
   *
   * @param \GuzzleHttp\Psr7\Response $response
   *   The response to store.
   * @param \GuzzleHttp\Psr7\Request $request
   *   The request, is used to generate the hash.
   * @param ?string $hash
   *   The custom hash value to use when storing.
   *   Useful when you need to store a modified response.
   */
  public function stubStoreResponse(Response $response, ?Request $request = NULL, ?string $hash = NULL) {
    $hash ??= $this->stubGetRequestHash($request);
    $filename = $this->stubGetRequestFilename($hash);
    $body = $response->getBody();
    $body->rewind();
    $content = $body->getContents();
    // Restore the seek to the beginning of the stream.
    $body->rewind();

    // Additional checks for the usage log mode.
    if ($this->options[self::OPTION_LOG_STORED_RESPONSES_USAGE_FILE]) {
      if (file_exists($filename)) {
        $storedContent = file_get_contents($filename);
        if ($storedContent == $content) {
          $usageOperation = 'check';
        }
        else {
          $usageOperation = 'update';
        }
      }
      else {
        $usageOperation = 'create';
      }
    }

    file_put_contents($filename, $content);

    $metadataFilename = $this->stubGetRequestFilename($hash, metadata: TRUE);
    $metadata = $this->prepareMetadata($request, $response);
    if (file_exists($metadataFilename)) {
      $metadataStoredContent = file_get_contents($metadataFilename);
      $metadataStored = Yaml::decode($metadataStoredContent);
      $metadata['tests'] = $metadataStored['tests'];
      // On the stubSetStoredResponse we have no request data, so copying it
      // from the stored response metadata.
      if (
        !isset($metadata['request'])
        && isset($metadataStored['request'])
      ) {
        $metadata['request'] = $metadataStored['request'];
      }
    }

    $this->ensureTestNameInMetadata($metadata);
    $this->stubSetStoredResponseMetadataByHash($metadata, $hash);
    if (isset($usageOperation)) {
      $this->stubLogResponseUsage($hash, $usageOperation);
    }
    $this->stubStoreRequestHashUsage($hash);
  }

  /**
   * Prepares the metadata array.
   *
   * @param \GuzzleHttp\Psr7\Request|null $request
   *   The request.
   * @param \GuzzleHttp\Psr7\Response $response
   *   The response.
   */
  protected function prepareMetadata(?Request $request, Response $response): array {
    $metadata = [
      'tests' => [],
      'response' => [
        'status' => $response->getStatusCode(),
      ],
    ];
    if ($this->options[self::OPTION_STORE_HEADERS]) {
      $metadata['response']['headers'] = $response->getHeaders();
      if ($this->options[self::OPTION_STORE_HEADERS_SKIP_KEYS]) {
        foreach ($this->options[self::OPTION_STORE_HEADERS_SKIP_KEYS] ?? [] as $header) {
          unset($metadata['response']['headers'][$header]);
        }
      }
    }
    if ($request) {
      $metadata['request'] = $this->stubGetRequestMetadata($request);
    }
    return $metadata;
  }

  /**
   * Gets the stored response filename from a Request.
   *
   * @param \GuzzleHttp\Psr7\Request $request
   *   A request.
   * @param bool $metadata
   *   A flag to return the metadata filename.
   *
   * @return string
   *   A full path to the stored response file.
   */
  public function stubGetRequestFilenameFromRequest(Request $request, bool $metadata = FALSE): string {
    return $this->stubGetRequestFilename($this->stubGetRequestHash($request), $metadata);
  }

  /**
   * Gets the request filename by the hash.
   *
   * @param string $hash
   *   A hash.
   * @param bool $metadata
   *   A flag to return the metadata file instead.
   *
   * @return string
   *   A full path to the file.
   */
  public function stubGetRequestFilename(string $hash, bool $metadata = FALSE): string {
    $directory = $this->stubGetResponsesStorageDirectory();
    $filename = $metadata ?
      "$directory/$hash.metadata.yml" :
      "$directory/$hash.txt";
    return $filename;
  }

  /**
   * Generates a hash for a request.
   *
   * @param \GuzzleHttp\Psr7\Request $request
   *   A request.
   *
   * @return string[]
   *   An associative array with the request metadata:
   *   - method: The request method.
   *   - uri: The request URI.
   *   - body: The request body, if not empty.
   */
  protected function stubGetRequestMetadata($request): array {
    $metadata = [
      'method' => $request->getMethod(),
      'uri' => $request->getUri()->__toString(),
    ];
    if ($this->stubContext) {
      $metadata['context'] = $this->stubContext;
    }
    $body = $request->getBody();
    if ($body->getSize() > 0) {
      $body->rewind();
      $metadata['body'] = $body->getContents();
    }
    return $metadata;
  }

  /**
   * Generates a hash for a request.
   *
   * @param \GuzzleHttp\Psr7\Request $request
   *   A request.
   *
   * @return string
   *   The generated hash.
   */
  public function stubGetRequestHash($request): string {
    return md5(json_encode($this->stubGetRequestMetadata($request)));
  }

  /**
   * Checks if the current test using PHPUnit.
   *
   * @return bool
   *   True if PHPUnit, false otherwise.
   */
  protected function stubIsPhpunitTest(): bool {
    return defined('PHPUNIT_COMPOSER_INSTALL');
  }

  /**
   * Gets the URI regular expression.
   *
   * @return string|null
   *   The URI regular expression.
   */
  public function stubGetUriRegexp(): ?string {
    return $this->options[self::OPTION_URI_REGEXP];
  }

  /**
   * Sets the URI regular expression.
   *
   * @param string|null $regexp
   *   The URI regular expression.
   */
  public function stubSetUriRegexp(?string $regexp): void {
    $this->options[self::OPTION_URI_REGEXP] = $regexp;
  }

  /**
   * Matches a request against the URI regular expression.
   *
   * @param \GuzzleHttp\Psr7\Request $request
   *   The request to match.
   *
   * @return bool
   *   TRUE if the request matches the URI regular expression, FALSE otherwise.
   */
  protected function stubMatchRequest(Request $request): bool {
    if ($this->options[self::OPTION_URI_REGEXP] ?? NULL) {
      return preg_match($this->options[self::OPTION_URI_REGEXP], $request->getUri()->__toString());
    }
    return TRUE;
  }

  /**
   * Returns the current responses storage directory.
   */
  public function stubGetResponsesStorageDirectory(): string {
    if ($this->responsesStorageDirectory === NULL) {
      throw new \Exception('To use the `store` and `mock` modes, you need to set the `responsesStorageDirectory` property.');
    }
    if (!is_dir($this->responsesStorageDirectory)) {
      mkdir($this->responsesStorageDirectory, recursive: TRUE);
    }
    return $this->responsesStorageDirectory;
  }

  /**
   * Sets the current responses storage directory.
   */
  public function stubSetResponsesStorageDirectory(string $directory): void {
    $this->responsesStorageDirectory = $directory;
  }

  /**
   * Adds the request hash to the container.
   *
   * @param string $hash
   *   A hash value.
   */
  protected function stubStoreRequestHashUsage(string $hash): void {
    $this->handledRequests[] = $hash;
  }

  /**
   * Returns the list of hashes of all mocked responses.
   *
   * @return array
   *   A list of hashes of all mocked responses.
   */
  public function stubGetHandledRequests(): array {
    return $this->handledRequests;
  }

  /**
   * Returns the last request response body.
   *
   * @return string
   *   The last response body.
   */
  public function stubGetLastResponse(int $delta = 0): string {
    $hashes = array_reverse($this->stubGetHandledRequests());
    return $this->stubGetStoredResponseByHash($hashes[$delta])->getBody()->getContents();
  }

  /**
   * Logs the response usage to the log file.
   *
   * @param string $hash
   *   A hash of the request.
   * @param string $operation
   *   The operation type: 'read', 'create', 'update', 'check', 'missing'.
   */
  private function stubLogResponseUsage(string $hash, string $operation): void {
    if (!$this->options[self::OPTION_LOG_STORED_RESPONSES_USAGE_FILE]) {
      return;
    }
    $entry = [
      "time" => microtime(TRUE),
      "hash" => $hash,
      "operation" => $operation,
      "test" => $this->stubGetTestName(),
    ];
    file_put_contents($this->options[self::OPTION_LOG_STORED_RESPONSES_USAGE_FILE], json_encode($entry) . "\n", FILE_APPEND);
  }

  /**
   * Ensures that the test name is present in the metadata.
   *
   * @param array $metadata
   *   The metadata array to check.
   *
   * @return bool
   *   TRUE if the test name is already present, FALSE otherwise.
   */
  protected function ensureTestNameInMetadata(array &$metadata) {
    $testName = $this->stubGetTestName();
    $metadata['tests'] ??= [];
    if (in_array($testName, $metadata['tests'])) {
      return TRUE;
    }
    $metadata['tests'][] = $testName;
    sort($metadata['tests']);
    return FALSE;
  }

  /**
   * Retrieves the response usage log entries from the log file as array.
   *
   * @return array
   *   The array of log entries.
   */
  public function stubGetResponseUsageLog(): array {
    if (
      empty($this->options[self::OPTION_LOG_STORED_RESPONSES_USAGE_FILE])
      || !file_exists($this->options[self::OPTION_LOG_STORED_RESPONSES_USAGE_FILE])
    ) {
      return [];
    }
    $log = file_get_contents($this->options[self::OPTION_LOG_STORED_RESPONSES_USAGE_FILE]);
    $entries = explode("\n", $log);
    $result = [];
    foreach ($entries as $entry) {
      if ($entry) {
        $result[] = json_decode($entry, TRUE);
      }
    }
    return $result;
  }

  /**
   * Removes the response usage log file.
   */
  public function stubRemoveResponseUsageLog(): void {
    unlink($this->options[self::OPTION_LOG_STORED_RESPONSES_USAGE_FILE]);
  }

  /**
   * Adds a custom response to the stack.
   *
   * @param \Psr\Http\Message\ResponseInterface $response
   *   A response to add.
   */
  public function stubAddCustomResponseToStack(ResponseInterface $response) {
    $this->stubCustomResponsesStack[] = $response;
  }

  /**
   * Sets a custom handler for the HTTP client.
   *
   * @param callable|null $handler
   *   A custom handler or null to unset the handler.
   */
  public function stubSetCustomHandler(?callable $handler) {
    $this->stubHandlerCustom = $handler;
    // Additionally reset the handler in the last handler stack, if set.
    if ($this->stubHandlerStackLast) {
      $this->stubHandlerStackLast->remove(self::HANDLER_NAME_CUSTOM);
      if ($handler) {
        $this->stubHandlerStackLast->unshift($this->stubHandlerCustom, self::HANDLER_NAME_CUSTOM);
      }
    }
  }

}

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc