Commit | Line | Data |
---|---|---|
2bee78a3 TO |
1 | <?php |
2 | ||
3 | namespace Civi\Test; | |
4 | ||
5 | use GuzzleHttp\HandlerStack; | |
12b478ef | 6 | use GuzzleHttp\MessageFormatter; |
2bee78a3 TO |
7 | use GuzzleHttp\Middleware; |
8 | ||
9 | /** | |
10 | * Class HttpTestTrait | |
11 | * | |
12 | * @package Civi\Test | |
13 | * | |
14 | * This trait provides helpers/assertions for testing Civi's HTTP interfaces, eg | |
15 | * | |
16 | * - createGuzzle() - Create HTTP client for sending requests to Civi. | |
17 | * - callApi4AjaxSuccess() - Use an HTTP client to send an AJAX-style request to APIv4 | |
18 | * - callApi4AjaxError() - Use an HTTP client to send an AJAX-style request to APIv4 | |
19 | * - assertStatusCode() - Check the status code. If it fails, output detailed information. | |
20 | * - assertContentType() - Check the content-type. If it fails, output detailed information. | |
21 | * | |
22 | * Use this in an E2E test for which you need to send inbound HTTP requests to Civi. | |
23 | * Alternatively, for a headless test which mocks outbound HTTP, see GuzzleTestTrait. | |
24 | */ | |
25 | trait HttpTestTrait { | |
26 | ||
27 | /** | |
28 | * List of HTTP requests that have been made by this test. | |
29 | * | |
30 | * @var array | |
31 | */ | |
32 | protected $httpHistory = []; | |
33 | ||
34 | /** | |
35 | * Create an HTTP client suitable for simulating AJAX requests. | |
36 | * | |
37 | * @param array $options | |
38 | * @return \GuzzleHttp\Client | |
39 | */ | |
40 | protected function createGuzzle($options = []) { | |
41 | $handler = HandlerStack::create(); | |
12b478ef | 42 | $handler->unshift(\CRM_Utils_GuzzleMiddleware::url(), 'civi_url'); |
2bee78a3 TO |
43 | $handler->push(Middleware::history($this->httpHistory), 'history'); |
44 | ||
12b478ef TO |
45 | if (getenv('DEBUG') >= 2) { |
46 | $handler->push(Middleware::log(new \CRM_Utils_EchoLogger(), new MessageFormatter(MessageFormatter::DEBUG)), 'log'); | |
47 | } | |
48 | elseif (getenv('DEBUG') >= 1) { | |
49 | $handler->push(\CRM_Utils_GuzzleMiddleware::curlLog(new \CRM_Utils_EchoLogger()), 'curl_log'); | |
50 | } | |
51 | ||
2bee78a3 TO |
52 | $defaults = [ |
53 | 'handler' => $handler, | |
54 | 'base_uri' => 'auto:', | |
55 | ]; | |
56 | ||
57 | $options = array_merge($defaults, $options); | |
58 | return new \GuzzleHttp\Client($options); | |
59 | } | |
60 | ||
61 | /** | |
62 | * @param string $entity | |
63 | * @param string $action | |
64 | * @param array $params | |
65 | * | |
66 | * @return mixed | |
67 | */ | |
68 | protected function callApi4AjaxSuccess(string $entity, string $action, $params = []) { | |
69 | $method = \CRM_Utils_String::startsWith($action, 'get') ? 'GET' : 'POST'; | |
70 | $response = $this->createGuzzle()->request($method, "civicrm/ajax/api4/$entity/$action", [ | |
71 | 'headers' => ['X-Requested-With' => 'XMLHttpRequest'], | |
72 | // This should probably be 'form_params', but 'query' is more representative of frontend. | |
73 | 'query' => ['params' => json_encode($params)], | |
74 | 'http_errors' => FALSE, | |
75 | ]); | |
76 | $this->assertContentType('application/json', $response); | |
77 | $this->assertStatusCode(200, $response); | |
78 | $result = json_decode((string) $response->getBody(), 1); | |
79 | if (json_last_error() !== JSON_ERROR_NONE) { | |
80 | $this->fail("Failed to decode APIv4 JSON.\n" . $this->formatFailure($response)); | |
81 | } | |
82 | return $result; | |
83 | } | |
84 | ||
85 | /** | |
86 | * @param string $entity | |
87 | * @param string $action | |
88 | * @param array $params | |
89 | * | |
90 | * @return mixed | |
91 | */ | |
92 | protected function callApi4AjaxError(string $entity, string $action, $params = []) { | |
93 | $method = \CRM_Utils_String::startsWith($action, 'get') ? 'GET' : 'POST'; | |
94 | $response = $this->createGuzzle()->request($method, "civicrm/ajax/api4/$entity/$action", [ | |
95 | 'headers' => ['X-Requested-With' => 'XMLHttpRequest'], | |
96 | // This should probably be 'form_params', but 'query' is more representative of frontend. | |
97 | 'query' => ['params' => json_encode($params)], | |
98 | 'http_errors' => FALSE, | |
99 | ]); | |
100 | $this->assertContentType('application/json', $response); | |
101 | $this->assertTrue($response->getStatusCode() >= 400, 'Should return an error' . $this->formatFailure($response)); | |
102 | $result = json_decode((string) $response->getBody(), 1); | |
103 | if (json_last_error() !== JSON_ERROR_NONE) { | |
104 | $this->fail("Failed to decode APIv4 JSON.\n" . $this->formatFailure($response)); | |
105 | } | |
106 | return $result; | |
107 | } | |
108 | ||
109 | /** | |
110 | * @param $expectCode | |
111 | * @param \Psr\Http\Message\ResponseInterface|NULL $response | |
112 | * If NULL, then it uses the last response. | |
113 | * | |
114 | * @return $this | |
115 | */ | |
116 | protected function assertStatusCode($expectCode, $response = NULL) { | |
117 | $response = $this->resolveResponse($response); | |
118 | $actualCode = $response->getStatusCode(); | |
119 | $fmt = $actualCode === $expectCode ? '' : $this->formatFailure($response); | |
120 | $this->assertEquals($expectCode, $actualCode, "Expected HTTP response $expectCode. Received HTTP response $actualCode.\n$fmt"); | |
121 | return $this; | |
122 | } | |
123 | ||
124 | /** | |
125 | * @param $expectType | |
126 | * @param \Psr\Http\Message\ResponseInterface|NULL $response | |
127 | * If NULL, then it uses the last response. | |
128 | * | |
129 | * @return $this | |
130 | */ | |
131 | protected function assertContentType($expectType, $response = NULL) { | |
132 | $response = $this->resolveResponse($response); | |
aa82ca9f | 133 | list($actualType) = explode(';', $response->getHeader('Content-Type')[0]); |
2bee78a3 | 134 | $fmt = $actualType === $expectType ? '' : $this->formatFailure($response); |
88b1511c | 135 | $this->assertEquals($expectType, $actualType, "Expected content-type $expectType. Received content-type $actualType.\n$fmt"); |
2bee78a3 TO |
136 | return $this; |
137 | } | |
138 | ||
139 | /** | |
140 | * @param \Psr\Http\Message\ResponseInterface|NULL $response | |
141 | * @return \Psr\Http\Message\ResponseInterface | |
142 | */ | |
143 | protected function resolveResponse($response) { | |
144 | return $response ?: $this->httpHistory[count($this->httpHistory) - 1]['response']; | |
145 | } | |
146 | ||
147 | /** | |
148 | * Given that an HTTP request has yielded a failed response, format a blurb | |
149 | * to summarize the details of the request+response. | |
150 | * | |
151 | * @param \Psr\Http\Message\ResponseInterface $response | |
152 | * | |
153 | * @return false|string | |
154 | */ | |
155 | protected function formatFailure(\Psr\Http\Message\ResponseInterface $response) { | |
156 | $details = []; | |
157 | ||
158 | $condenseArray = function($v) { | |
159 | if (is_array($v) && count($v) === 1 && isset($v[0])) { | |
160 | return $v[0]; | |
161 | } | |
162 | else { | |
163 | return $v; | |
164 | } | |
165 | }; | |
166 | ||
167 | /** @var \Psr\Http\Message\RequestInterface $request */ | |
168 | $request = NULL; | |
169 | foreach ($this->httpHistory as $item) { | |
170 | if ($item['response'] === $response) { | |
171 | $request = $item['request']; | |
172 | break; | |
173 | } | |
174 | } | |
175 | ||
176 | if ($request) { | |
177 | $details['request'] = [ | |
178 | 'uri' => (string) $request->getUri(), | |
179 | 'method' => $request->getMethod(), | |
180 | // Most headers only have one item. JSON pretty-printer adds several newlines. This output is meant for dev's reading the error-log. | |
181 | 'headers' => array_map($condenseArray, $request->getHeaders()), | |
182 | 'body' => (string) $request->getBody(), | |
183 | ]; | |
184 | } | |
185 | ||
186 | $details['response'] = [ | |
187 | 'status' => $response->getStatusCode() . ' ' . $response->getReasonPhrase(), | |
188 | // Most headers only have one item. JSON pretty-printer adds several newlines. This output is meant for dev's reading the error-log. | |
189 | 'headers' => array_map($condenseArray, $response->getHeaders()), | |
190 | 'body' => (string) $response->getBody(), | |
191 | ]; | |
192 | ||
193 | // If we get a full HTML document, then it'll be hard to read the error messages. Give an alternate rendition. | |
194 | if (preg_match(';\<(!DOCTYPE|HTML);', $details['response']['body'])) { | |
195 | // $details['bodyText'] = strip_tags($details['body']); // too much <style> noise | |
196 | $details['response']['body'] = \CRM_Utils_String::htmlToText($details['response']['body']); | |
197 | } | |
198 | ||
199 | return json_encode($details, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); | |
200 | } | |
201 | ||
202 | } |