5 use GuzzleHttp\HandlerStack
;
6 use GuzzleHttp\MessageFormatter
;
7 use GuzzleHttp\Middleware
;
14 * This trait provides helpers/assertions for testing Civi's HTTP interfaces, eg
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.
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.
28 * List of HTTP requests that have been made by this test.
32 protected $httpHistory = [];
35 * Create an HTTP client suitable for simulating AJAX requests.
37 * The client may include some mix of these middlewares:
39 * @see \CRM_Utils_GuzzleMiddleware::authx()
40 * @see \CRM_Utils_GuzzleMiddleware::url()
41 * @see \CRM_Utils_GuzzleMiddleware::curlLog()
42 * @see Middleware::history()
43 * @see Middleware::log()
45 * @param array $options
46 * @return \GuzzleHttp\Client
48 protected function createGuzzle($options = []) {
49 $handler = HandlerStack
::create();
50 $handler->unshift(\CRM_Utils_GuzzleMiddleware
::authx(), 'civi_authx');
51 $handler->unshift(\CRM_Utils_GuzzleMiddleware
::url(), 'civi_url');
52 $handler->push(Middleware
::history($this->httpHistory
), 'history');
54 if (getenv('DEBUG') >= 2) {
55 $handler->push(Middleware
::log(new \
CRM_Utils_EchoLogger(), new MessageFormatter(MessageFormatter
::DEBUG
)), 'log');
57 elseif (getenv('DEBUG') >= 1) {
58 $handler->push(\CRM_Utils_GuzzleMiddleware
::curlLog(new \
CRM_Utils_EchoLogger()), 'curl_log');
62 'handler' => $handler,
63 'base_uri' => 'auto:',
66 $options = array_merge($defaults, $options);
67 return new \GuzzleHttp\
Client($options);
71 * @param string $entity
72 * @param string $action
73 * @param array $params
77 protected function callApi4AjaxSuccess(string $entity, string $action, $params = []) {
78 $method = \CRM_Utils_String
::startsWith($action, 'get') ?
'GET' : 'POST';
79 $response = $this->createGuzzle()->request($method, "civicrm/ajax/api4/$entity/$action", [
80 'headers' => ['X-Requested-With' => 'XMLHttpRequest'],
81 // This should probably be 'form_params', but 'query' is more representative of frontend.
82 'query' => ['params' => json_encode($params)],
83 'http_errors' => FALSE,
85 $this->assertContentType('application/json', $response);
86 $this->assertStatusCode(200, $response);
87 $result = json_decode((string) $response->getBody(), 1);
88 if (json_last_error() !== JSON_ERROR_NONE
) {
89 $this->fail("Failed to decode APIv4 JSON.\n" . $this->formatFailure($response));
95 * @param string $entity
96 * @param string $action
97 * @param array $params
101 protected function callApi4AjaxError(string $entity, string $action, $params = []) {
102 $method = \CRM_Utils_String
::startsWith($action, 'get') ?
'GET' : 'POST';
103 $response = $this->createGuzzle()->request($method, "civicrm/ajax/api4/$entity/$action", [
104 'headers' => ['X-Requested-With' => 'XMLHttpRequest'],
105 // This should probably be 'form_params', but 'query' is more representative of frontend.
106 'query' => ['params' => json_encode($params)],
107 'http_errors' => FALSE,
109 $this->assertContentType('application/json', $response);
110 $this->assertTrue($response->getStatusCode() >= 400, 'Should return an error' . $this->formatFailure($response));
111 $result = json_decode((string) $response->getBody(), 1);
112 if (json_last_error() !== JSON_ERROR_NONE
) {
113 $this->fail("Failed to decode APIv4 JSON.\n" . $this->formatFailure($response));
120 * @param \Psr\Http\Message\ResponseInterface|null $response
121 * If NULL, then it uses the last response.
125 protected function assertStatusCode($expectCode, $response = NULL) {
126 $response = $this->resolveResponse($response);
127 $actualCode = $response->getStatusCode();
128 $fmt = $actualCode === $expectCode ?
'' : $this->formatFailure($response);
129 $this->assertEquals($expectCode, $actualCode, "Expected HTTP response $expectCode. Received HTTP response $actualCode.\n$fmt");
135 * @param \Psr\Http\Message\ResponseInterface|null $response
136 * If NULL, then it uses the last response.
140 protected function assertContentType($expectType, $response = NULL) {
141 $response = $this->resolveResponse($response);
142 list($actualType) = explode(';', $response->getHeader('Content-Type')[0]);
143 $fmt = $actualType === $expectType ?
'' : $this->formatFailure($response);
144 $this->assertEquals($expectType, $actualType, "Expected content-type $expectType. Received content-type $actualType.\n$fmt");
149 * @param \Psr\Http\Message\ResponseInterface|null $response
150 * @return \Psr\Http\Message\ResponseInterface
152 protected function resolveResponse($response) {
153 return $response ?
: $this->httpHistory
[count($this->httpHistory
) - 1]['response'];
157 * Given that an HTTP request has yielded a failed response, format a blurb
158 * to summarize the details of the request+response.
160 * @param \Psr\Http\Message\ResponseInterface $response
162 * @return false|string
164 protected function formatFailure(\Psr\Http\Message\ResponseInterface
$response) {
167 $condenseArray = function($v) {
168 if (is_array($v) && count($v) === 1 && isset($v[0])) {
176 /** @var \Psr\Http\Message\RequestInterface $request */
178 foreach ($this->httpHistory
as $item) {
179 if ($item['response'] === $response) {
180 $request = $item['request'];
186 $details['request'] = [
187 'uri' => (string) $request->getUri(),
188 'method' => $request->getMethod(),
189 // Most headers only have one item. JSON pretty-printer adds several newlines. This output is meant for dev's reading the error-log.
190 'headers' => array_map($condenseArray, $request->getHeaders()),
191 'body' => (string) $request->getBody(),
195 $details['response'] = [
196 'status' => $response->getStatusCode() . ' ' . $response->getReasonPhrase(),
197 // Most headers only have one item. JSON pretty-printer adds several newlines. This output is meant for dev's reading the error-log.
198 'headers' => array_map($condenseArray, $response->getHeaders()),
199 'body' => (string) $response->getBody(),
202 // If we get a full HTML document, then it'll be hard to read the error messages. Give an alternate rendition.
203 if (preg_match(';\<(!DOCTYPE|HTML);', $details['response']['body'])) {
204 // $details['bodyText'] = strip_tags($details['body']); // too much <style> noise
205 $details['response']['body'] = \CRM_Utils_String
::htmlToText($details['response']['body']);
208 return json_encode($details, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES
);