Merge pull request #21374 from ufundo/event-custom-tokens
[civicrm-core.git] / Civi / Test / HttpTestTrait.php
1 <?php
2
3 namespace Civi\Test;
4
5 use GuzzleHttp\HandlerStack;
6 use GuzzleHttp\MessageFormatter;
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();
42 $handler->unshift(\CRM_Utils_GuzzleMiddleware::url(), 'civi_url');
43 $handler->push(Middleware::history($this->httpHistory), 'history');
44
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
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);
133 list($actualType) = explode(';', $response->getHeader('Content-Type')[0]);
134 $fmt = $actualType === $expectType ? '' : $this->formatFailure($response);
135 $this->assertEquals($expectType, $actualType, "Expected content-type $expectType. Received content-type $actualType.\n$fmt");
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 }