TestResponse.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * This file is part of Hyperf.
  5. *
  6. * @link https://www.hyperf.io
  7. * @document https://hyperf.wiki
  8. * @contact group@hyperf.io
  9. * @license https://github.com/hyperf/hyperf/blob/master/LICENSE
  10. */
  11. namespace Hyperf\Testing\Http;
  12. use ArrayAccess;
  13. use Hyperf\Collection\Arr;
  14. use Hyperf\Collection\Collection;
  15. use Hyperf\HttpServer\Response;
  16. use Hyperf\Macroable\Macroable;
  17. use Hyperf\Stringable\Str;
  18. use Hyperf\Tappable\Tappable;
  19. use Hyperf\Testing\AssertableJsonString;
  20. use Hyperf\Testing\Constraint\SeeInOrder;
  21. use Hyperf\Testing\Fluent\AssertableJson;
  22. use LogicException;
  23. use PHPUnit\Framework\Assert as PHPUnit;
  24. use Psr\Http\Message\ResponseInterface;
  25. use Swow\Psr7\Message\ResponsePlusInterface;
  26. use Symfony\Component\HttpFoundation\StreamedResponse;
  27. use Throwable;
  28. /**
  29. * @mixin Response
  30. */
  31. class TestResponse implements ArrayAccess
  32. {
  33. use Concerns\AssertsStatusCodes, Tappable, Macroable {
  34. __call as macroCall;
  35. }
  36. protected ?array $decoded = null;
  37. /**
  38. * The streamed content of the response.
  39. *
  40. * @var null|string
  41. */
  42. protected $streamedContent;
  43. public function __construct(protected ResponseInterface $response)
  44. {
  45. }
  46. /**
  47. * Handle dynamic calls into macros or pass missing methods to the base response.
  48. *
  49. * @param string $method
  50. * @param array $args
  51. * @return mixed
  52. */
  53. public function __call($method, $args)
  54. {
  55. if (static::hasMacro($method)) {
  56. return $this->macroCall($method, $args);
  57. }
  58. return $this->response->{$method}(...$args);
  59. }
  60. /**
  61. * Dynamically access base response parameters.
  62. *
  63. * @param string $key
  64. * @return mixed
  65. */
  66. public function __get($key)
  67. {
  68. return $this->response->{$key};
  69. }
  70. /**
  71. * Proxy isset() checks to the underlying base response.
  72. *
  73. * @param string $key
  74. * @return bool
  75. */
  76. public function __isset($key)
  77. {
  78. return isset($this->response->{$key});
  79. }
  80. /**
  81. * Get the content of the response.
  82. */
  83. public function getContent(): string
  84. {
  85. return $this->response->getBody()->getContents();
  86. }
  87. /**
  88. * Assert that the given string matches the response content.
  89. *
  90. * @param string $value
  91. * @return $this
  92. */
  93. public function assertContent($value)
  94. {
  95. PHPUnit::assertSame($value, $this->getContent());
  96. return $this;
  97. }
  98. /**
  99. * Assert that the given string matches the streamed response content.
  100. *
  101. * @param string $value
  102. * @return $this
  103. */
  104. public function assertStreamedContent($value)
  105. {
  106. PHPUnit::assertSame($value, $this->streamedContent());
  107. return $this;
  108. }
  109. /**
  110. * Assert that the given string or array of strings are contained within the response.
  111. *
  112. * @param array|string $value
  113. * @param bool $escape
  114. * @return $this
  115. */
  116. public function assertSee($value, $escape = true)
  117. {
  118. $value = Arr::wrap($value);
  119. $values = $escape ? array_map(fn ($value) => htmlspecialchars($value ?? '', ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8', true), $value) : $value;
  120. foreach ($values as $value) {
  121. PHPUnit::assertStringContainsString((string) $value, $this->getContent());
  122. }
  123. return $this;
  124. }
  125. /**
  126. * Assert that the given strings are contained in order within the response.
  127. *
  128. * @param bool $escape
  129. * @return $this
  130. */
  131. public function assertSeeInOrder(array $values, $escape = true)
  132. {
  133. $values = $escape ? array_map(fn ($value) => htmlspecialchars($value ?? '', ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8', true), $values) : $values;
  134. PHPUnit::assertThat($values, new SeeInOrder($this->getContent()));
  135. return $this;
  136. }
  137. /**
  138. * Assert that the given string or array of strings are contained within the response text.
  139. *
  140. * @param array|string $value
  141. * @param bool $escape
  142. * @return $this
  143. */
  144. public function assertSeeText($value, $escape = true)
  145. {
  146. $value = Arr::wrap($value);
  147. $values = $escape ? array_map(fn ($value) => htmlspecialchars($value ?? '', ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8', true), $value) : $value;
  148. $content = strip_tags($this->getContent());
  149. foreach ($values as $value) {
  150. PHPUnit::assertStringContainsString((string) $value, $content);
  151. }
  152. return $this;
  153. }
  154. /**
  155. * Assert that the given strings are contained in order within the response text.
  156. *
  157. * @param bool $escape
  158. * @return $this
  159. */
  160. public function assertSeeTextInOrder(array $values, $escape = true)
  161. {
  162. $values = $escape ? array_map(fn ($value) => htmlspecialchars($value ?? '', ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8', true), $values) : $values;
  163. PHPUnit::assertThat($values, new SeeInOrder(strip_tags($this->getContent())));
  164. return $this;
  165. }
  166. /**
  167. * Assert that the given string or array of strings are not contained within the response.
  168. *
  169. * @param array|string $value
  170. * @param bool $escape
  171. * @return $this
  172. */
  173. public function assertDontSee($value, $escape = true)
  174. {
  175. $value = Arr::wrap($value);
  176. $values = $escape ? array_map(fn ($value) => htmlspecialchars($value ?? '', ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8', true), $value) : $value;
  177. foreach ($values as $value) {
  178. PHPUnit::assertStringNotContainsString((string) $value, $this->getContent());
  179. }
  180. return $this;
  181. }
  182. /**
  183. * Assert that the given string or array of strings are not contained within the response text.
  184. *
  185. * @param array|string $value
  186. * @param bool $escape
  187. * @return $this
  188. */
  189. public function assertDontSeeText($value, $escape = true)
  190. {
  191. $value = Arr::wrap($value);
  192. $values = $escape ? array_map(fn ($value) => htmlspecialchars($value ?? '', ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8', true), $value) : $value;
  193. $content = strip_tags($this->getContent());
  194. foreach ($values as $value) {
  195. PHPUnit::assertStringNotContainsString((string) $value, $content);
  196. }
  197. return $this;
  198. }
  199. /**
  200. * Assert that the response is a superset of the given JSON.
  201. *
  202. * @param array|callable $value
  203. * @param bool $strict
  204. * @return $this
  205. */
  206. public function assertJson($value, $strict = false)
  207. {
  208. $json = $this->decodeResponseJson();
  209. if (is_array($value)) {
  210. $json->assertSubset($value, $strict);
  211. } else {
  212. $assert = AssertableJson::fromAssertableJsonString($json);
  213. $value($assert);
  214. if (Arr::isAssoc($assert->toArray())) {
  215. $assert->interacted();
  216. }
  217. }
  218. return $this;
  219. }
  220. /**
  221. * Assert that the expected value and type exists at the given path in the response.
  222. *
  223. * @param string $path
  224. * @param mixed $expect
  225. * @return $this
  226. */
  227. public function assertJsonPath($path, $expect)
  228. {
  229. $this->decodeResponseJson()->assertPath($path, $expect);
  230. return $this;
  231. }
  232. /**
  233. * Assert that the response has the exact given JSON.
  234. *
  235. * @return $this
  236. */
  237. public function assertExactJson(array $data)
  238. {
  239. $this->decodeResponseJson()->assertExact($data);
  240. return $this;
  241. }
  242. /**
  243. * Assert that the response has the similar JSON as given.
  244. *
  245. * @return $this
  246. */
  247. public function assertSimilarJson(array $data)
  248. {
  249. $this->decodeResponseJson()->assertSimilar($data);
  250. return $this;
  251. }
  252. /**
  253. * Assert that the response contains the given JSON fragment.
  254. *
  255. * @return $this
  256. */
  257. public function assertJsonFragment(array $data)
  258. {
  259. $this->decodeResponseJson()->assertFragment($data);
  260. return $this;
  261. }
  262. /**
  263. * Assert that the response does not contain the given JSON fragment.
  264. *
  265. * @param bool $exact
  266. * @return $this
  267. */
  268. public function assertJsonMissing(array $data, $exact = false)
  269. {
  270. $this->decodeResponseJson()->assertMissing($data, $exact);
  271. return $this;
  272. }
  273. /**
  274. * Assert that the response does not contain the exact JSON fragment.
  275. *
  276. * @return $this
  277. */
  278. public function assertJsonMissingExact(array $data)
  279. {
  280. $this->decodeResponseJson()->assertMissingExact($data);
  281. return $this;
  282. }
  283. /**
  284. * Assert that the response does not contain the given path.
  285. *
  286. * @return $this
  287. */
  288. public function assertJsonMissingPath(string $path)
  289. {
  290. $this->decodeResponseJson()->assertMissingPath($path);
  291. return $this;
  292. }
  293. /**
  294. * Assert that the response has a given JSON structure.
  295. *
  296. * @param null|array $responseData
  297. * @return $this
  298. */
  299. public function assertJsonStructure(?array $structure = null, $responseData = null)
  300. {
  301. $this->decodeResponseJson()->assertStructure($structure, $responseData);
  302. return $this;
  303. }
  304. /**
  305. * Assert that the response JSON has the expected count of items at the given key.
  306. *
  307. * @param null|string $key
  308. * @return $this
  309. */
  310. public function assertJsonCount(int $count, $key = null)
  311. {
  312. $this->decodeResponseJson()->assertCount($count, $key);
  313. return $this;
  314. }
  315. /**
  316. * Assert that the response has the given JSON validation errors.
  317. *
  318. * @param array|string $errors
  319. * @param string $responseKey
  320. * @return $this
  321. */
  322. public function assertJsonValidationErrors($errors, $responseKey = 'errors')
  323. {
  324. $errors = Arr::wrap($errors);
  325. PHPUnit::assertNotEmpty($errors, 'No validation errors were provided.');
  326. $jsonErrors = Arr::get($this->json(), $responseKey) ?? [];
  327. $errorMessage = $jsonErrors
  328. ? 'Response has the following JSON validation errors:'
  329. . PHP_EOL . PHP_EOL . json_encode($jsonErrors, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . PHP_EOL
  330. : 'Response does not have JSON validation errors.';
  331. foreach ($errors as $key => $value) {
  332. if (is_int($key)) {
  333. $this->assertJsonValidationErrorFor($value, $responseKey);
  334. continue;
  335. }
  336. $this->assertJsonValidationErrorFor($key, $responseKey);
  337. foreach (Arr::wrap($value) as $expectedMessage) {
  338. $errorMissing = true;
  339. foreach (Arr::wrap($jsonErrors[$key]) as $jsonErrorMessage) {
  340. if (Str::contains($jsonErrorMessage, $expectedMessage)) {
  341. $errorMissing = false;
  342. break;
  343. }
  344. }
  345. }
  346. if ($errorMissing) { /* @phpstan-ignore-line */
  347. PHPUnit::fail(
  348. "Failed to find a validation error in the response for key and message: '{$key}' => '{$expectedMessage}'" . PHP_EOL . PHP_EOL . $errorMessage /* @phpstan-ignore-line */
  349. );
  350. }
  351. }
  352. return $this;
  353. }
  354. /**
  355. * Assert the response has any JSON validation errors for the given key.
  356. *
  357. * @param string $key
  358. * @param string $responseKey
  359. * @return $this
  360. */
  361. public function assertJsonValidationErrorFor($key, $responseKey = 'errors')
  362. {
  363. $jsonErrors = Arr::get($this->json(), $responseKey) ?? [];
  364. $errorMessage = $jsonErrors
  365. ? 'Response has the following JSON validation errors:'
  366. . PHP_EOL . PHP_EOL . json_encode($jsonErrors, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . PHP_EOL
  367. : 'Response does not have JSON validation errors.';
  368. PHPUnit::assertArrayHasKey(
  369. $key,
  370. $jsonErrors,
  371. "Failed to find a validation error in the response for key: '{$key}'" . PHP_EOL . PHP_EOL . $errorMessage
  372. );
  373. return $this;
  374. }
  375. /**
  376. * Assert that the response has no JSON validation errors for the given keys.
  377. *
  378. * @param null|array|string $keys
  379. * @param string $responseKey
  380. * @return $this
  381. */
  382. public function assertJsonMissingValidationErrors($keys = null, $responseKey = 'errors')
  383. {
  384. if ($this->getContent() === '') {
  385. PHPUnit::assertTrue(true);
  386. return $this;
  387. }
  388. $json = $this->json();
  389. if (! Arr::has($json, $responseKey)) {
  390. PHPUnit::assertTrue(true);
  391. return $this;
  392. }
  393. $errors = Arr::get($json, $responseKey, []);
  394. if (is_null($keys) && count($errors) > 0) {
  395. PHPUnit::fail(
  396. 'Response has unexpected validation errors: ' . PHP_EOL . PHP_EOL
  397. . json_encode($errors, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)
  398. );
  399. }
  400. foreach (Arr::wrap($keys) as $key) {
  401. PHPUnit::assertFalse(
  402. isset($errors[$key]),
  403. "Found unexpected validation error for key: '{$key}'"
  404. );
  405. }
  406. return $this;
  407. }
  408. /**
  409. * Assert that the given key is a JSON array.
  410. *
  411. * @param null|mixed $key
  412. * @return $this
  413. */
  414. public function assertJsonIsArray($key = null)
  415. {
  416. $data = $this->json($key);
  417. $encodedData = json_encode($data);
  418. PHPUnit::assertTrue(
  419. is_array($data)
  420. && str_starts_with($encodedData, '[')
  421. && str_ends_with($encodedData, ']')
  422. );
  423. return $this;
  424. }
  425. /**
  426. * Assert that the given key is a JSON object.
  427. *
  428. * @param null|mixed $key
  429. * @return $this
  430. */
  431. public function assertJsonIsObject($key = null)
  432. {
  433. $data = $this->json($key);
  434. $encodedData = json_encode($data);
  435. PHPUnit::assertTrue(
  436. is_array($data)
  437. && str_starts_with($encodedData, '{')
  438. && str_ends_with($encodedData, '}')
  439. );
  440. return $this;
  441. }
  442. /**
  443. * Validate and return the decoded response JSON.
  444. *
  445. * @return AssertableJsonString
  446. *
  447. * @throws Throwable
  448. */
  449. public function decodeResponseJson()
  450. {
  451. $testJson = new AssertableJsonString($this->getContent());
  452. $decodedResponse = $testJson->json();
  453. if (is_null($decodedResponse) || $decodedResponse === false) {
  454. $exception = $this->exception ?? null;
  455. $exception && throw $exception;
  456. PHPUnit::fail('Invalid JSON was returned from the route.');
  457. }
  458. return $testJson;
  459. }
  460. /**
  461. * Get the JSON decoded body of the response as an array or scalar value.
  462. *
  463. * @param null|string $key
  464. */
  465. public function json($key = null): mixed
  466. {
  467. return $this->decodeResponseJson()->json($key);
  468. }
  469. /**
  470. * Get the JSON decoded body of the response as a collection.
  471. *
  472. * @param null|string $key
  473. */
  474. public function collect($key = null): Collection
  475. {
  476. return Collection::make($this->json($key));
  477. }
  478. public function offsetExists(mixed $offset): bool
  479. {
  480. return isset($this->json()[$offset]);
  481. }
  482. public function offsetGet(mixed $offset): mixed
  483. {
  484. return $this->json()[$offset];
  485. }
  486. public function offsetSet(mixed $offset, mixed $value): void
  487. {
  488. throw new LogicException('Response data may not be mutated using array access.');
  489. }
  490. public function offsetUnset(mixed $offset): void
  491. {
  492. throw new LogicException('Response data may not be mutated using array access.');
  493. }
  494. public static function fromBaseResponse(ResponsePlusInterface $response)
  495. {
  496. return new static(new Response($response));
  497. }
  498. /**
  499. * Assert that the response has a successful status code.
  500. */
  501. public function assertSuccessful(): self
  502. {
  503. PHPUnit::assertTrue(
  504. $this->isSuccessful(),
  505. $this->statusMessageWithDetails('>=200, <300', $this->getStatusCode())
  506. );
  507. return $this;
  508. }
  509. /**
  510. * Assert that the response has the given status code.
  511. *
  512. * @param int $status
  513. * @return $this
  514. */
  515. public function assertStatus($status): self
  516. {
  517. $message = $this->statusMessageWithDetails($status, $actual = $this->getStatusCode());
  518. PHPUnit::assertSame($actual, $status, $message);
  519. return $this;
  520. }
  521. /**
  522. * Is response successful?
  523. *
  524. * @final
  525. */
  526. public function isSuccessful(): bool
  527. {
  528. return $this->getStatusCode() >= 200 && $this->getStatusCode() < 300;
  529. }
  530. /**
  531. * Was there a server side error?
  532. *
  533. * @final
  534. */
  535. public function isServerError(): bool
  536. {
  537. return $this->getStatusCode() >= 500 && $this->getStatusCode() < 600;
  538. }
  539. /**
  540. * Assert that the response is a server error.
  541. */
  542. public function assertServerError(): self
  543. {
  544. PHPUnit::assertTrue(
  545. $this->isServerError(),
  546. $this->statusMessageWithDetails('>=500, < 600', $this->getStatusCode())
  547. );
  548. return $this;
  549. }
  550. public function getStatusCode(): int
  551. {
  552. return $this->response->getStatusCode();
  553. }
  554. /**
  555. * Get the streamed content from the response.
  556. *
  557. * @return string
  558. */
  559. public function streamedContent()
  560. {
  561. if (! is_null($this->streamedContent)) {
  562. return $this->streamedContent;
  563. }
  564. if (! $this->response instanceof StreamedResponse) {
  565. PHPUnit::fail('The response is not a streamed response.');
  566. }
  567. ob_start(function (string $buffer): string {
  568. $this->streamedContent .= $buffer;
  569. return '';
  570. });
  571. $this->sendContent();
  572. ob_end_clean();
  573. return (string) $this->streamedContent;
  574. }
  575. /**
  576. * Sends content for the current web response.
  577. *
  578. * @return $this
  579. */
  580. public function sendContent(): static
  581. {
  582. echo $this->streamedContent;
  583. return $this;
  584. }
  585. /**
  586. * Get an assertion message for a status assertion containing extra details when available.
  587. *
  588. * @param int|string $expected
  589. * @param int|string $actual
  590. */
  591. protected function statusMessageWithDetails($expected, $actual): string
  592. {
  593. return "Expected response status code [{$expected}] but received {$actual}.";
  594. }
  595. }