MockResponse.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322
  1. <?php
  2. /*
  3. * This file is part of the Symfony package.
  4. *
  5. * (c) Fabien Potencier <fabien@symfony.com>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Symfony\Component\HttpClient\Response;
  11. use Symfony\Component\HttpClient\Chunk\ErrorChunk;
  12. use Symfony\Component\HttpClient\Chunk\FirstChunk;
  13. use Symfony\Component\HttpClient\Exception\InvalidArgumentException;
  14. use Symfony\Component\HttpClient\Exception\TransportException;
  15. use Symfony\Component\HttpClient\Internal\ClientState;
  16. use Symfony\Contracts\HttpClient\ResponseInterface;
  17. /**
  18. * A test-friendly response.
  19. *
  20. * @author Nicolas Grekas <p@tchwork.com>
  21. */
  22. class MockResponse implements ResponseInterface, StreamableInterface
  23. {
  24. use CommonResponseTrait;
  25. use TransportResponseTrait {
  26. doDestruct as public __destruct;
  27. }
  28. private $body;
  29. private $requestOptions = [];
  30. private $requestUrl;
  31. private $requestMethod;
  32. private static $mainMulti;
  33. private static $idSequence = 0;
  34. /**
  35. * @param string|string[]|iterable $body The response body as a string or an iterable of strings,
  36. * yielding an empty string simulates an idle timeout,
  37. * exceptions are turned to TransportException
  38. *
  39. * @see ResponseInterface::getInfo() for possible info, e.g. "response_headers"
  40. */
  41. public function __construct($body = '', array $info = [])
  42. {
  43. $this->body = is_iterable($body) ? $body : (string) $body;
  44. $this->info = $info + ['http_code' => 200] + $this->info;
  45. if (!isset($info['response_headers'])) {
  46. return;
  47. }
  48. $responseHeaders = [];
  49. foreach ($info['response_headers'] as $k => $v) {
  50. foreach ((array) $v as $v) {
  51. $responseHeaders[] = (\is_string($k) ? $k.': ' : '').$v;
  52. }
  53. }
  54. $this->info['response_headers'] = [];
  55. self::addResponseHeaders($responseHeaders, $this->info, $this->headers);
  56. }
  57. /**
  58. * Returns the options used when doing the request.
  59. */
  60. public function getRequestOptions(): array
  61. {
  62. return $this->requestOptions;
  63. }
  64. /**
  65. * Returns the URL used when doing the request.
  66. */
  67. public function getRequestUrl(): string
  68. {
  69. return $this->requestUrl;
  70. }
  71. /**
  72. * Returns the method used when doing the request.
  73. */
  74. public function getRequestMethod(): string
  75. {
  76. return $this->requestMethod;
  77. }
  78. /**
  79. * {@inheritdoc}
  80. */
  81. public function getInfo(string $type = null)
  82. {
  83. return null !== $type ? $this->info[$type] ?? null : $this->info;
  84. }
  85. /**
  86. * {@inheritdoc}
  87. */
  88. public function cancel(): void
  89. {
  90. $this->info['canceled'] = true;
  91. $this->info['error'] = 'Response has been canceled.';
  92. $this->body = null;
  93. }
  94. /**
  95. * {@inheritdoc}
  96. */
  97. protected function close(): void
  98. {
  99. $this->inflate = null;
  100. $this->body = [];
  101. }
  102. /**
  103. * @internal
  104. */
  105. public static function fromRequest(string $method, string $url, array $options, ResponseInterface $mock): self
  106. {
  107. $response = new self([]);
  108. $response->requestOptions = $options;
  109. $response->id = ++self::$idSequence;
  110. $response->shouldBuffer = $options['buffer'] ?? true;
  111. $response->initializer = static function (self $response) {
  112. return \is_array($response->body[0] ?? null);
  113. };
  114. $response->info['redirect_count'] = 0;
  115. $response->info['redirect_url'] = null;
  116. $response->info['start_time'] = microtime(true);
  117. $response->info['http_method'] = $method;
  118. $response->info['http_code'] = 0;
  119. $response->info['user_data'] = $options['user_data'] ?? null;
  120. $response->info['url'] = $url;
  121. if ($mock instanceof self) {
  122. $mock->requestOptions = $response->requestOptions;
  123. $mock->requestMethod = $method;
  124. $mock->requestUrl = $url;
  125. }
  126. self::writeRequest($response, $options, $mock);
  127. $response->body[] = [$options, $mock];
  128. return $response;
  129. }
  130. /**
  131. * {@inheritdoc}
  132. */
  133. protected static function schedule(self $response, array &$runningResponses): void
  134. {
  135. if (!$response->id) {
  136. throw new InvalidArgumentException('MockResponse instances must be issued by MockHttpClient before processing.');
  137. }
  138. $multi = self::$mainMulti ?? self::$mainMulti = new ClientState();
  139. if (!isset($runningResponses[0])) {
  140. $runningResponses[0] = [$multi, []];
  141. }
  142. $runningResponses[0][1][$response->id] = $response;
  143. }
  144. /**
  145. * {@inheritdoc}
  146. */
  147. protected static function perform(ClientState $multi, array &$responses): void
  148. {
  149. foreach ($responses as $response) {
  150. $id = $response->id;
  151. if (null === $response->body) {
  152. // Canceled response
  153. $response->body = [];
  154. } elseif ([] === $response->body) {
  155. // Error chunk
  156. $multi->handlesActivity[$id][] = null;
  157. $multi->handlesActivity[$id][] = null !== $response->info['error'] ? new TransportException($response->info['error']) : null;
  158. } elseif (null === $chunk = array_shift($response->body)) {
  159. // Last chunk
  160. $multi->handlesActivity[$id][] = null;
  161. $multi->handlesActivity[$id][] = array_shift($response->body);
  162. } elseif (\is_array($chunk)) {
  163. // First chunk
  164. try {
  165. $offset = 0;
  166. $chunk[1]->getStatusCode();
  167. $chunk[1]->getHeaders(false);
  168. self::readResponse($response, $chunk[0], $chunk[1], $offset);
  169. $multi->handlesActivity[$id][] = new FirstChunk();
  170. $buffer = $response->requestOptions['buffer'] ?? null;
  171. if ($buffer instanceof \Closure && $response->content = $buffer($response->headers) ?: null) {
  172. $response->content = \is_resource($response->content) ? $response->content : fopen('php://temp', 'w+');
  173. }
  174. } catch (\Throwable $e) {
  175. $multi->handlesActivity[$id][] = null;
  176. $multi->handlesActivity[$id][] = $e;
  177. }
  178. } else {
  179. // Data or timeout chunk
  180. $multi->handlesActivity[$id][] = $chunk;
  181. }
  182. }
  183. }
  184. /**
  185. * {@inheritdoc}
  186. */
  187. protected static function select(ClientState $multi, float $timeout): int
  188. {
  189. return 42;
  190. }
  191. /**
  192. * Simulates sending the request.
  193. */
  194. private static function writeRequest(self $response, array $options, ResponseInterface $mock)
  195. {
  196. $onProgress = $options['on_progress'] ?? static function () {};
  197. $response->info += $mock->getInfo() ?: [];
  198. // simulate "size_upload" if it is set
  199. if (isset($response->info['size_upload'])) {
  200. $response->info['size_upload'] = 0.0;
  201. }
  202. // simulate "total_time" if it is not set
  203. if (!isset($response->info['total_time'])) {
  204. $response->info['total_time'] = microtime(true) - $response->info['start_time'];
  205. }
  206. // "notify" DNS resolution
  207. $onProgress(0, 0, $response->info);
  208. // consume the request body
  209. if (\is_resource($body = $options['body'] ?? '')) {
  210. $data = stream_get_contents($body);
  211. if (isset($response->info['size_upload'])) {
  212. $response->info['size_upload'] += \strlen($data);
  213. }
  214. } elseif ($body instanceof \Closure) {
  215. while ('' !== $data = $body(16372)) {
  216. if (!\is_string($data)) {
  217. throw new TransportException(sprintf('Return value of the "body" option callback must be string, "%s" returned.', get_debug_type($data)));
  218. }
  219. // "notify" upload progress
  220. if (isset($response->info['size_upload'])) {
  221. $response->info['size_upload'] += \strlen($data);
  222. }
  223. $onProgress(0, 0, $response->info);
  224. }
  225. }
  226. }
  227. /**
  228. * Simulates reading the response.
  229. */
  230. private static function readResponse(self $response, array $options, ResponseInterface $mock, int &$offset)
  231. {
  232. $onProgress = $options['on_progress'] ?? static function () {};
  233. // populate info related to headers
  234. $info = $mock->getInfo() ?: [];
  235. $response->info['http_code'] = ($info['http_code'] ?? 0) ?: $mock->getStatusCode() ?: 200;
  236. $response->addResponseHeaders($info['response_headers'] ?? [], $response->info, $response->headers);
  237. $dlSize = isset($response->headers['content-encoding']) || 'HEAD' === $response->info['http_method'] || \in_array($response->info['http_code'], [204, 304], true) ? 0 : (int) ($response->headers['content-length'][0] ?? 0);
  238. $response->info = [
  239. 'start_time' => $response->info['start_time'],
  240. 'user_data' => $response->info['user_data'],
  241. 'http_code' => $response->info['http_code'],
  242. ] + $info + $response->info;
  243. if (!isset($response->info['total_time'])) {
  244. $response->info['total_time'] = microtime(true) - $response->info['start_time'];
  245. }
  246. // "notify" headers arrival
  247. $onProgress(0, $dlSize, $response->info);
  248. // cast response body to activity list
  249. $body = $mock instanceof self ? $mock->body : $mock->getContent(false);
  250. if (!\is_string($body)) {
  251. foreach ($body as $chunk) {
  252. if ('' === $chunk = (string) $chunk) {
  253. // simulate an idle timeout
  254. $response->body[] = new ErrorChunk($offset, sprintf('Idle timeout reached for "%s".', $response->info['url']));
  255. } else {
  256. $response->body[] = $chunk;
  257. $offset += \strlen($chunk);
  258. // "notify" download progress
  259. $onProgress($offset, $dlSize, $response->info);
  260. }
  261. }
  262. } elseif ('' !== $body) {
  263. $response->body[] = $body;
  264. $offset = \strlen($body);
  265. }
  266. if (!isset($response->info['total_time'])) {
  267. $response->info['total_time'] = microtime(true) - $response->info['start_time'];
  268. }
  269. // "notify" completion
  270. $onProgress($offset, $dlSize, $response->info);
  271. if ($dlSize && $offset !== $dlSize) {
  272. throw new TransportException(sprintf('Transfer closed with %d bytes remaining to read.', $dlSize - $offset));
  273. }
  274. }
  275. }