diff --git a/src/Symfony/Component/HttpClient/CHANGELOG.md b/src/Symfony/Component/HttpClient/CHANGELOG.md index 88a5cc4b533b3..96ecbaf25c4ef 100644 --- a/src/Symfony/Component/HttpClient/CHANGELOG.md +++ b/src/Symfony/Component/HttpClient/CHANGELOG.md @@ -5,6 +5,7 @@ CHANGELOG --- * Remove implementing `Http\Message\RequestFactory` from `HttplugClient` + * Add `HarFileResponseFactory` testing utility, allow to replay responses from `.har` files 6.4 --- diff --git a/src/Symfony/Component/HttpClient/Test/HarFileResponseFactory.php b/src/Symfony/Component/HttpClient/Test/HarFileResponseFactory.php new file mode 100644 index 0000000000000..7265709a07efe --- /dev/null +++ b/src/Symfony/Component/HttpClient/Test/HarFileResponseFactory.php @@ -0,0 +1,97 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpClient\Test; + +use Symfony\Component\HttpClient\Exception\TransportException; +use Symfony\Component\HttpClient\Response\MockResponse; +use Symfony\Contracts\HttpClient\ResponseInterface; + +/** + * See: https://w3c.github.io/web-performance/specs/HAR/Overview.html. + * + * @author Gary PEGEOT + */ +class HarFileResponseFactory +{ + public function __construct(private string $archiveFile) + { + } + + public function setArchiveFile(string $archiveFile): void + { + $this->archiveFile = $archiveFile; + } + + public function __invoke(string $method, string $url, array $options): ResponseInterface + { + if (!is_file($this->archiveFile)) { + throw new \InvalidArgumentException(sprintf('Invalid file path provided: "%s".', $this->archiveFile)); + } + + $json = json_decode(json: file_get_contents($this->archiveFile), associative: true, flags: \JSON_THROW_ON_ERROR); + + foreach ($json['log']['entries'] as $entry) { + /** + * @var array{status: int, headers: array, content: array} $response + * @var array{method: string, url: string, postData: array} $request + */ + ['response' => $response, 'request' => $request, 'startedDateTime' => $startedDateTime] = $entry; + + $body = $this->getContent($response['content']); + $entryMethod = $request['method']; + $entryUrl = $request['url']; + $requestBody = $options['body'] ?? null; + + if ($method !== $entryMethod || $url !== $entryUrl) { + continue; + } + + if (null !== $requestBody && $requestBody !== $this->getContent($request['postData'] ?? [])) { + continue; + } + + $info = [ + 'http_code' => $response['status'], + 'http_method' => $entryMethod, + 'response_headers' => [], + 'start_time' => strtotime($startedDateTime), + 'url' => $entryUrl, + ]; + + /** @var array{name: string, value: string} $header */ + foreach ($response['headers'] as $header) { + ['name' => $name, 'value' => $value] = $header; + + $info['response_headers'][$name][] = $value; + } + + return new MockResponse($body, $info); + } + + throw new TransportException(sprintf('File "%s" does not contain a response for HTTP request "%s" "%s".', $this->archiveFile, $method, $url)); + } + + /** + * @param array{text: string, encoding: string} $content + */ + private function getContent(array $content): string + { + $text = $content['text'] ?? ''; + $encoding = $content['encoding'] ?? null; + + return match ($encoding) { + 'base64' => base64_decode($text), + null => $text, + default => throw new \InvalidArgumentException(sprintf('Unsupported encoding "%s", currently only base64 is supported.', $encoding)), + }; + } +} diff --git a/src/Symfony/Component/HttpClient/Tests/Fixtures/har/graphql.github.io_archive.har b/src/Symfony/Component/HttpClient/Tests/Fixtures/har/graphql.github.io_archive.har new file mode 100644 index 0000000000000..76f8ab9756a3b --- /dev/null +++ b/src/Symfony/Component/HttpClient/Tests/Fixtures/har/graphql.github.io_archive.har @@ -0,0 +1,644 @@ +{ + "log": { + "version": "1.2", + "creator": { + "name": "Firefox", + "version": "114.0.2" + }, + "browser": { + "name": "Firefox", + "version": "114.0.2" + }, + "pages": [ + { + "startedDateTime": "2023-06-28T15:09:54.445+02:00", + "id": "page_3", + "title": "SWAPI GraphQL API", + "pageTimings": { + "onContentLoad": -1, + "onLoad": -1 + } + } + ], + "entries": [ + { + "pageref": "page_3", + "startedDateTime": "2023-06-28T15:09:54.445+02:00", + "request": { + "bodySize": 0, + "method": "OPTIONS", + "url": "https://swapi-graphql.netlify.app/graphql", + "httpVersion": "HTTP/2", + "headers": [ + { + "name": "Host", + "value": "swapi-graphql.netlify.app" + }, + { + "name": "User-Agent", + "value": "Mozilla/5.0 (Windows NT 10.0; rv:114.0) Gecko/20100101 Firefox/114.0" + }, + { + "name": "Accept", + "value": "*/*" + }, + { + "name": "Accept-Language", + "value": "en-US,en;q=0.5" + }, + { + "name": "Accept-Encoding", + "value": "gzip, deflate, br" + }, + { + "name": "Access-Control-Request-Method", + "value": "POST" + }, + { + "name": "Access-Control-Request-Headers", + "value": "content-type" + }, + { + "name": "Referer", + "value": "https://graphql.github.io/" + }, + { + "name": "Origin", + "value": "https://graphql.github.io" + }, + { + "name": "DNT", + "value": "1" + }, + { + "name": "Connection", + "value": "keep-alive" + }, + { + "name": "Sec-Fetch-Dest", + "value": "empty" + }, + { + "name": "Sec-Fetch-Mode", + "value": "cors" + }, + { + "name": "Sec-Fetch-Site", + "value": "cross-site" + } + ], + "cookies": [], + "queryString": [], + "headersSize": 503 + }, + "response": { + "status": 204, + "statusText": "No Content", + "httpVersion": "HTTP/2", + "headers": [ + { + "name": "access-control-allow-headers", + "value": "content-type" + }, + { + "name": "access-control-allow-methods", + "value": "GET,HEAD,PUT,PATCH,POST,DELETE" + }, + { + "name": "access-control-allow-origin", + "value": "*" + }, + { + "name": "age", + "value": "0" + }, + { + "name": "cache-control", + "value": "no-cache" + }, + { + "name": "date", + "value": "Wed, 28 Jun 2023 13:09:54 GMT" + }, + { + "name": "server", + "value": "Netlify" + }, + { + "name": "strict-transport-security", + "value": "max-age=31536000; includeSubDomains; preload" + }, + { + "name": "vary", + "value": "Access-Control-Request-Headers" + }, + { + "name": "x-nf-request-id", + "value": "01H411ZVQVBT0YQBM4HVM13M6B" + }, + { + "name": "x-powered-by", + "value": "Express" + }, + { + "name": "X-Firefox-Spdy", + "value": "h2" + } + ], + "cookies": [], + "content": { + "mimeType": "text/plain", + "size": 0, + "text": "" + }, + "redirectURL": "", + "headersSize": 449, + "bodySize": 449 + }, + "cache": {}, + "timings": { + "blocked": 0, + "dns": 22, + "connect": 26, + "ssl": 35, + "send": 0, + "wait": 332, + "receive": 0 + }, + "time": 415, + "_securityState": "secure", + "serverIPAddress": "2a05:d014:275:cb01::c8", + "connection": "443" + }, + { + "pageref": "page_3", + "startedDateTime": "2023-06-28T15:09:54.863+02:00", + "request": { + "bodySize": 140, + "method": "POST", + "url": "https://swapi-graphql.netlify.app/graphql", + "httpVersion": "HTTP/2", + "headers": [ + { + "name": "Host", + "value": "swapi-graphql.netlify.app" + }, + { + "name": "User-Agent", + "value": "Mozilla/5.0 (Windows NT 10.0; rv:114.0) Gecko/20100101 Firefox/114.0" + }, + { + "name": "Accept", + "value": "application/json" + }, + { + "name": "Accept-Language", + "value": "en-US,en;q=0.5" + }, + { + "name": "Accept-Encoding", + "value": "gzip, deflate, br" + }, + { + "name": "Referer", + "value": "https://graphql.github.io/" + }, + { + "name": "Content-Type", + "value": "application/json" + }, + { + "name": "Content-Length", + "value": "140" + }, + { + "name": "Origin", + "value": "https://graphql.github.io" + }, + { + "name": "DNT", + "value": "1" + }, + { + "name": "Connection", + "value": "keep-alive" + }, + { + "name": "Sec-Fetch-Dest", + "value": "empty" + }, + { + "name": "Sec-Fetch-Mode", + "value": "cors" + }, + { + "name": "Sec-Fetch-Site", + "value": "cross-site" + }, + { + "name": "TE", + "value": "trailers" + } + ], + "cookies": [], + "queryString": [], + "headersSize": 483, + "postData": { + "mimeType": "application/json", + "params": [], + "text": "{\"query\":\"{\\n allPlanets(first: 5) {\\n edges {\\n node {\\n name\\n population\\n }\\n }\\n totalCount\\n }\\n}\"}" + } + }, + "response": { + "status": 200, + "statusText": "OK", + "httpVersion": "HTTP/2", + "headers": [ + { + "name": "access-control-allow-origin", + "value": "*" + }, + { + "name": "age", + "value": "0" + }, + { + "name": "cache-control", + "value": "no-cache" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Wed, 28 Jun 2023 13:09:54 GMT" + }, + { + "name": "etag", + "value": "W/\"121-7dPlPIsXvusgXWd85SPlxqektH8\"" + }, + { + "name": "server", + "value": "Netlify" + }, + { + "name": "strict-transport-security", + "value": "max-age=31536000; includeSubDomains; preload" + }, + { + "name": "x-nf-request-id", + "value": "01H411ZW2AD35VKV3BVKC0B1BR" + }, + { + "name": "x-powered-by", + "value": "Express" + }, + { + "name": "content-length", + "value": "289" + }, + { + "name": "X-Firefox-Spdy", + "value": "h2" + } + ], + "cookies": [], + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 289, + "text": "{\"data\":{\"allPlanets\":{\"edges\":[{\"node\":{\"name\":\"Tatooine\",\"population\":200000}},{\"node\":{\"name\":\"Alderaan\",\"population\":2000000000}},{\"node\":{\"name\":\"Yavin IV\",\"population\":1000}},{\"node\":{\"name\":\"Hoth\",\"population\":null}},{\"node\":{\"name\":\"Dagobah\",\"population\":null}}],\"totalCount\":60}}}" + }, + "redirectURL": "", + "headersSize": 408, + "bodySize": 697 + }, + "cache": {}, + "timings": { + "blocked": 0, + "dns": 0, + "connect": 0, + "ssl": 0, + "send": 0, + "wait": 140, + "receive": 0 + }, + "time": 140, + "_securityState": "secure", + "serverIPAddress": "2a05:d014:275:cb01::c8", + "connection": "443" + }, + { + "pageref": "page_3", + "startedDateTime": "2023-06-28T15:10:20.623+02:00", + "request": { + "bodySize": 0, + "method": "OPTIONS", + "url": "https://swapi-graphql.netlify.app/graphql", + "httpVersion": "HTTP/2", + "headers": [ + { + "name": "Host", + "value": "swapi-graphql.netlify.app" + }, + { + "name": "User-Agent", + "value": "Mozilla/5.0 (Windows NT 10.0; rv:114.0) Gecko/20100101 Firefox/114.0" + }, + { + "name": "Accept", + "value": "*/*" + }, + { + "name": "Accept-Language", + "value": "en-US,en;q=0.5" + }, + { + "name": "Accept-Encoding", + "value": "gzip, deflate, br" + }, + { + "name": "Access-Control-Request-Method", + "value": "POST" + }, + { + "name": "Access-Control-Request-Headers", + "value": "content-type" + }, + { + "name": "Referer", + "value": "https://graphql.github.io/" + }, + { + "name": "Origin", + "value": "https://graphql.github.io" + }, + { + "name": "DNT", + "value": "1" + }, + { + "name": "Connection", + "value": "keep-alive" + }, + { + "name": "Sec-Fetch-Dest", + "value": "empty" + }, + { + "name": "Sec-Fetch-Mode", + "value": "cors" + }, + { + "name": "Sec-Fetch-Site", + "value": "cross-site" + }, + { + "name": "TE", + "value": "trailers" + } + ], + "cookies": [], + "queryString": [], + "headersSize": 503 + }, + "response": { + "status": 204, + "statusText": "No Content", + "httpVersion": "HTTP/2", + "headers": [ + { + "name": "access-control-allow-headers", + "value": "content-type" + }, + { + "name": "access-control-allow-methods", + "value": "GET,HEAD,PUT,PATCH,POST,DELETE" + }, + { + "name": "access-control-allow-origin", + "value": "*" + }, + { + "name": "age", + "value": "0" + }, + { + "name": "cache-control", + "value": "no-cache" + }, + { + "name": "date", + "value": "Wed, 28 Jun 2023 13:10:20 GMT" + }, + { + "name": "server", + "value": "Netlify" + }, + { + "name": "strict-transport-security", + "value": "max-age=31536000; includeSubDomains; preload" + }, + { + "name": "vary", + "value": "Access-Control-Request-Headers" + }, + { + "name": "x-nf-request-id", + "value": "01H4120N7CTHFH0JGR2CYKWG5S" + }, + { + "name": "x-powered-by", + "value": "Express" + }, + { + "name": "X-Firefox-Spdy", + "value": "h2" + } + ], + "cookies": [], + "content": { + "mimeType": "text/plain", + "size": 0, + "text": "" + }, + "redirectURL": "", + "headersSize": 449, + "bodySize": 449 + }, + "cache": {}, + "timings": { + "blocked": 0, + "dns": 0, + "connect": 0, + "ssl": 0, + "send": 0, + "wait": 164, + "receive": 0 + }, + "time": 164, + "_securityState": "secure", + "serverIPAddress": "2a05:d014:275:cb01::c8", + "connection": "443" + }, + { + "pageref": "page_3", + "startedDateTime": "2023-06-28T15:10:20.830+02:00", + "request": { + "bodySize": 137, + "method": "POST", + "url": "https://swapi-graphql.netlify.app/graphql", + "httpVersion": "HTTP/2", + "headers": [ + { + "name": "Host", + "value": "swapi-graphql.netlify.app" + }, + { + "name": "User-Agent", + "value": "Mozilla/5.0 (Windows NT 10.0; rv:114.0) Gecko/20100101 Firefox/114.0" + }, + { + "name": "Accept", + "value": "application/json" + }, + { + "name": "Accept-Language", + "value": "en-US,en;q=0.5" + }, + { + "name": "Accept-Encoding", + "value": "gzip, deflate, br" + }, + { + "name": "Referer", + "value": "https://graphql.github.io/" + }, + { + "name": "Content-Type", + "value": "application/json" + }, + { + "name": "Content-Length", + "value": "137" + }, + { + "name": "Origin", + "value": "https://graphql.github.io" + }, + { + "name": "DNT", + "value": "1" + }, + { + "name": "Connection", + "value": "keep-alive" + }, + { + "name": "Sec-Fetch-Dest", + "value": "empty" + }, + { + "name": "Sec-Fetch-Mode", + "value": "cors" + }, + { + "name": "Sec-Fetch-Site", + "value": "cross-site" + }, + { + "name": "TE", + "value": "trailers" + } + ], + "cookies": [], + "queryString": [], + "headersSize": 483, + "postData": { + "mimeType": "application/json", + "params": [], + "text": "{\"query\":\"{\\n allFilms(first: 5) {\\n edges {\\n node {\\n title\\n director\\n }\\n }\\n totalCount\\n }\\n}\"}" + } + }, + "response": { + "status": 200, + "statusText": "OK", + "httpVersion": "HTTP/2", + "headers": [ + { + "name": "access-control-allow-origin", + "value": "*" + }, + { + "name": "age", + "value": "0" + }, + { + "name": "cache-control", + "value": "no-cache" + }, + { + "name": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "name": "date", + "value": "Wed, 28 Jun 2023 13:10:20 GMT" + }, + { + "name": "etag", + "value": "W/\"17f-TuxAgZCdCuvdKoW7g+r5k6aKy6Q\"" + }, + { + "name": "server", + "value": "Netlify" + }, + { + "name": "strict-transport-security", + "value": "max-age=31536000; includeSubDomains; preload" + }, + { + "name": "x-nf-request-id", + "value": "01H4120NDYJ17J0Q5KKSZRWDY5" + }, + { + "name": "x-powered-by", + "value": "Express" + }, + { + "name": "content-length", + "value": "383" + }, + { + "name": "X-Firefox-Spdy", + "value": "h2" + } + ], + "cookies": [], + "content": { + "mimeType": "application/json; charset=utf-8", + "size": 383, + "text": "{\"data\":{\"allFilms\":{\"edges\":[{\"node\":{\"title\":\"A New Hope\",\"director\":\"George Lucas\"}},{\"node\":{\"title\":\"The Empire Strikes Back\",\"director\":\"Irvin Kershner\"}},{\"node\":{\"title\":\"Return of the Jedi\",\"director\":\"Richard Marquand\"}},{\"node\":{\"title\":\"The Phantom Menace\",\"director\":\"George Lucas\"}},{\"node\":{\"title\":\"Attack of the Clones\",\"director\":\"George Lucas\"}}],\"totalCount\":6}}}" + }, + "redirectURL": "", + "headersSize": 408, + "bodySize": 791 + }, + "cache": {}, + "timings": { + "blocked": -1, + "dns": 0, + "connect": 0, + "ssl": 0, + "send": 0, + "wait": 204, + "receive": 0 + }, + "time": 204, + "_securityState": "secure", + "serverIPAddress": "2a05:d014:275:cb01::c8", + "connection": "443" + } + ] + } +} diff --git a/src/Symfony/Component/HttpClient/Tests/Fixtures/har/invalid_archive.har b/src/Symfony/Component/HttpClient/Tests/Fixtures/har/invalid_archive.har new file mode 100644 index 0000000000000..01f95c30d044c --- /dev/null +++ b/src/Symfony/Component/HttpClient/Tests/Fixtures/har/invalid_archive.har @@ -0,0 +1,7 @@ +{ + "log": { + "version": "1.2", + "creator": { + "name": "Firefox", + "version": "114.0.2" + }, diff --git a/src/Symfony/Component/HttpClient/Tests/Fixtures/har/symfony.com_archive.har b/src/Symfony/Component/HttpClient/Tests/Fixtures/har/symfony.com_archive.har new file mode 100644 index 0000000000000..17a762b3a7ac3 --- /dev/null +++ b/src/Symfony/Component/HttpClient/Tests/Fixtures/har/symfony.com_archive.har @@ -0,0 +1,278 @@ +{ + "log": { + "version": "1.2", + "creator": { + "name": "Firefox", + "version": "114.0.2" + }, + "browser": { + "name": "Firefox", + "version": "114.0.2" + }, + "pages": [ + { + "startedDateTime": "2023-06-28T11:34:32.977+02:00", + "id": "page_1", + "title": "https://symfony.com/releases.json", + "pageTimings": { + "onContentLoad": 95, + "onLoad": 95 + } + } + ], + "entries": [ + { + "pageref": "page_1", + "startedDateTime": "2023-06-28T11:34:32.977+02:00", + "request": { + "bodySize": 0, + "method": "GET", + "url": "https://symfony.com/releases.json", + "httpVersion": "HTTP/2", + "headers": [ + { + "name": "Host", + "value": "symfony.com" + }, + { + "name": "User-Agent", + "value": "Mozilla/5.0 (Windows NT 10.0; rv:114.0) Gecko/20100101 Firefox/114.0" + }, + { + "name": "Accept", + "value": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8" + }, + { + "name": "Accept-Language", + "value": "en-US,en;q=0.5" + }, + { + "name": "Accept-Encoding", + "value": "gzip, deflate, br" + }, + { + "name": "DNT", + "value": "1" + }, + { + "name": "Connection", + "value": "keep-alive" + }, + { + "name": "Cookie", + "value": "symfony=7a16e52fc684d8e5055efc0a26foobar" + }, + { + "name": "Upgrade-Insecure-Requests", + "value": "1" + }, + { + "name": "Sec-Fetch-Dest", + "value": "document" + }, + { + "name": "Sec-Fetch-Mode", + "value": "navigate" + }, + { + "name": "Sec-Fetch-Site", + "value": "none" + }, + { + "name": "Sec-Fetch-User", + "value": "?1" + }, + { + "name": "TE", + "value": "trailers" + } + ], + "cookies": [ + { + "name": "symfony", + "value": "7a16e52fc684d8e5055efc0a26foobar" + } + ], + "queryString": [], + "headersSize": 502 + }, + "response": { + "status": 200, + "statusText": "OK", + "httpVersion": "HTTP/2", + "headers": [ + { + "name": "date", + "value": "Wed, 28 Jun 2023 09:34:33 GMT" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "age", + "value": "3386" + }, + { + "name": "cache-control", + "value": "public, s-maxage=3600" + }, + { + "name": "permissions-policy", + "value": "interest-cohort=()" + }, + { + "name": "referrer-policy", + "value": "no-referrer-when-downgrade" + }, + { + "name": "strict-transport-security", + "value": "max-age=0" + }, + { + "name": "x-content-digest", + "value": "end2d56611523c1d92b2f26cb5bfaa3ecd" + }, + { + "name": "x-debug-info", + "value": "eyJyZXRyaWVzIjowfQ==" + }, + { + "name": "x-frame-options", + "value": "deny" + }, + { + "name": "x-platform-cache", + "value": "BYPASS" + }, + { + "name": "x-platform-cluster", + "value": "lgei2rga6u3mm-master-7rqtwti" + }, + { + "name": "x-platform-processor", + "value": "mso3wvcgfq4362basj2ljveqpa" + }, + { + "name": "x-platform-router", + "value": "g3iqazcdo2xbnpwdfh436nvpna" + }, + { + "name": "x-xss-protection", + "value": "1; mode=block" + }, + { + "name": "traceresponse", + "value": "00-176cc8eb3b09a6152a8c01ff93f0eb0a-9bc17c308ca3972e-00" + }, + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "name": "report-to", + "value": "{\"endpoints\":[{\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v3?s=NqP2oqRxFSI3UbKL0%2FS0IhDnsOYRGZqHRF6ukXnzswafvnKiWllF%2BZ7lu0Bjeuegm4HNA%2FyvkrEnsoqT%2BJsUDfRtRUw2ipUoPBCO8leI2VGwgNG706IsgHn6jiwAYNUukjoToUoTG0l7\"}],\"group\":\"cf-nel\",\"max_age\":604800}" + }, + { + "name": "nel", + "value": "{\"success_fraction\":0,\"report_to\":\"cf-nel\",\"max_age\":604800}" + }, + { + "name": "server", + "value": "cloudflare" + }, + { + "name": "cf-ray", + "value": "7de4ef402b9d22a9-CDG" + }, + { + "name": "content-encoding", + "value": "br" + }, + { + "name": "X-Firefox-Spdy", + "value": "h2" + } + ], + "cookies": [], + "content": { + "mimeType": "application/vnd.mozilla.json.view", + "size": 367, + "encoding": "base64", + "text": "eyJzeW1mb255X3ZlcnNpb25zIjp7Imx0cyI6IjUuNC4yNSIsInN0YWJsZSI6IjYuMy4xIiwibmV4dCI6IjYuNC4wLURFViJ9LCJsYXRlc3Rfc3RhYmxlX3ZlcnNpb24iOiI2LjMiLCJzdXBwb3J0ZWRfdmVyc2lvbnMiOlsiNS40IiwiNi4yIiwiNi4zIl0sIm1haW50YWluZWRfdmVyc2lvbnMiOlsiNS40IiwiNi4yIiwiNi4zIiwiNi40IiwiNy4wIl0sInNlY3VyaXR5X21haW50YWluZWRfdmVyc2lvbnMiOlsiNC40Il0sImZsZXhfc3VwcG9ydGVkX3ZlcnNpb25zIjpbIjMuNCIsIjQuMCIsIjQuMSIsIjQuMiIsIjQuMyIsIjQuNCIsIjUuMCIsIjUuMSIsIjUuMiIsIjUuMyIsIjUuNCIsIjYuMCIsIjYuMSIsIjYuMiIsIjYuMyIsIjYuNCIsIjcuMCJdfQ==" + }, + "redirectURL": "", + "headersSize": 1100, + "bodySize": 1270 + }, + "cache": {}, + "timings": { + "blocked": 0, + "dns": 0, + "connect": 0, + "ssl": 0, + "send": 0, + "wait": 35, + "receive": 0 + }, + "time": 35, + "_securityState": "secure", + "serverIPAddress": "2606:4700:20::ac43:4826", + "connection": "443" + }, + { + "pageref": "page_1", + "startedDateTime": "2023-06-28T11:34:33.073+02:00", + "request": { + "bodySize": 0, + "method": "GET", + "url": "https://symfony.com/favicon.ico", + "httpVersion": "", + "headers": [ + { + "name": "Host", + "value": "symfony.com" + }, + { + "name": "User-Agent", + "value": "Mozilla/5.0 (Windows NT 10.0; rv:114.0) Gecko/20100101 Firefox/114.0" + }, + { + "name": "Accept", + "value": "image/avif,image/webp,*/*" + }, + { + "name": "Accept-Language", + "value": "en-US,en;q=0.5" + }, + { + "name": "Accept-Encoding", + "value": "gzip, deflate, br" + }, + { + "name": "Referer", + "value": "https://symfony.com/" + } + ], + "cookies": [], + "queryString": [], + "headersSize": 0 + }, + "response": { + "status": 0, + "statusText": "", + "httpVersion": "", + "headers": [], + "cookies": [], + "content": {}, + "redirectURL": "", + "headersSize": 0, + "bodySize": -1 + }, + "cache": {}, + "timings": {}, + "time": 0 + } + ] + } +} diff --git a/src/Symfony/Component/HttpClient/Tests/Test/HarFileResponseFactoryTest.php b/src/Symfony/Component/HttpClient/Tests/Test/HarFileResponseFactoryTest.php new file mode 100644 index 0000000000000..d5a01253a49d5 --- /dev/null +++ b/src/Symfony/Component/HttpClient/Tests/Test/HarFileResponseFactoryTest.php @@ -0,0 +1,90 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpClient\Tests\Test; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpClient\Exception\TransportException; +use Symfony\Component\HttpClient\MockHttpClient; +use Symfony\Component\HttpClient\Test\HarFileResponseFactory; + +class HarFileResponseFactoryTest extends TestCase +{ + private string $fixtureDir; + + protected function setUp(): void + { + $this->fixtureDir = \dirname(__DIR__).'/Fixtures/har'; + } + + public function testResponseGeneration() + { + $factory = new HarFileResponseFactory("{$this->fixtureDir}/symfony.com_archive.har"); + $client = new MockHttpClient($factory, 'https://symfony.com'); + + $response = $client->request('GET', '/releases.json'); + + $this->assertSame(200, $response->getStatusCode()); + + $body = $response->toArray(); + $headers = $response->getHeaders(); + + $this->assertCount(23, $headers); + $this->assertArrayHasKey('symfony_versions', $body); + } + + public function testResponseGenerationWithPayload() + { + $factory = new HarFileResponseFactory("{$this->fixtureDir}/graphql.github.io_archive.har"); + $client = new MockHttpClient($factory, 'https://swapi-graphql.netlify.app'); + $query = <<<'GRAPHQL' +{ + allFilms(first: 5) { + edges { + node { + title + director + } + } + totalCount + } +} +GRAPHQL; + + $response = $client->request('POST', '/graphql', [ + 'json' => ['query' => $query], + ]); + + $this->assertSame(200, $response->getStatusCode()); + + $body = $response->toArray(); + // In fixture file first response is "allPlanets" + $this->assertArrayHasKey('allFilms', $body['data']); + } + + public function testFactoryThrowsWhenUnableToMatchResponse() + { + $this->expectException(TransportException::class); + $factory = new HarFileResponseFactory("{$this->fixtureDir}/symfony.com_archive.har"); + $client = new MockHttpClient($factory, 'https://symfony.com'); + + $client->request('GET', '/not-found'); + } + + public function testFactoryThrowsWhenJsonIsInvalid() + { + $this->expectException(\JsonException::class); + $factory = new HarFileResponseFactory("{$this->fixtureDir}/invalid_archive.har"); + $client = new MockHttpClient($factory, 'https://symfony.com'); + + $client->request('GET', '/releases.json'); + } +}