xref: /PHP-8.1/sapi/fpm/tests/response.inc (revision aa061cd4)
1<?php
2
3namespace FPM;
4
5class Response
6{
7    const HEADER_SEPARATOR = "\r\n\r\n";
8
9    /**
10     * @var array
11     */
12    private $data;
13
14    /**
15     * @var string
16     */
17    private $rawData;
18
19    /**
20     * @var string
21     */
22    private $rawHeaders;
23
24    /**
25     * @var string
26     */
27    private $rawBody;
28
29    /**
30     * @var array
31     */
32    private $headers;
33
34    /**
35     * @var bool
36     */
37    private $valid;
38
39    /**
40     * @var bool
41     */
42    private $expectInvalid;
43
44    /**
45     * @param string|array|null $data
46     * @param bool              $expectInvalid
47     */
48    public function __construct($data = null, $expectInvalid = false)
49    {
50        if ( ! is_array($data)) {
51            $data = [
52                'response'     => $data,
53                'err_response' => null,
54                'out_response' => $data,
55            ];
56        }
57
58        $this->data          = $data;
59        $this->expectInvalid = $expectInvalid;
60    }
61
62    /**
63     * @param mixed  $body
64     * @param string $contentType
65     *
66     * @return Response
67     */
68    public function expectBody($body, $contentType = 'text/html')
69    {
70        if ($multiLine = is_array($body)) {
71            $body = implode("\n", $body);
72        }
73
74        if (
75            $this->checkIfValid() &&
76            $this->checkDefaultHeaders($contentType) &&
77            $body !== $this->rawBody
78        ) {
79            if ($multiLine) {
80                $this->error(
81                    "==> The expected body:\n$body\n" .
82                    "==> does not match the actual body:\n$this->rawBody"
83                );
84            } else {
85                $this->error(
86                    "The expected body '$body' does not match actual body '$this->rawBody'"
87                );
88            }
89        }
90
91        return $this;
92    }
93
94    /**
95     * Expect that one of the processes in json status process list has a field with value that
96     * matches the supplied pattern.
97     *
98     * @param string $fieldName
99     * @param string $pattern
100     *
101     * @return Response
102     */
103    public function expectJsonBodyPatternForStatusProcessField(string $fieldName, string $pattern)
104    {
105        $rawData = $this->getBody('application/json');
106        $data = json_decode($rawData, true);
107        if (empty($data['processes']) || !is_array($data['processes'])) {
108            $this->error(
109                "The body data is not a valid status json containing processes field '$rawData'"
110            );
111        }
112        foreach ($data['processes'] as $process) {
113            if (preg_match('|' . $pattern . '|', $process[$fieldName]) !== false) {
114                return $this;
115            }
116        }
117
118        $this->error(
119            "No field $fieldName matched pattern $pattern for any process in status data '$rawData'"
120        );
121
122        return $this;
123    }
124
125    /**
126     * @return Response
127     */
128    public function expectEmptyBody()
129    {
130        return $this->expectBody('');
131    }
132
133    /**
134     * Expect header in the response.
135     *
136     * @param string $name  Header name.
137     * @param string $value Header value.
138     *
139     * @return Response
140     */
141    public function expectHeader($name, $value): Response
142    {
143        $this->checkHeader($name, $value);
144
145        return $this;
146    }
147
148    /**
149     * Expect error in the response.
150     *
151     * @param string|null $errorMessage Expected error message.
152     *
153     * @return Response
154     */
155    public function expectError($errorMessage): Response
156    {
157        $errorData = $this->getErrorData();
158        if ($errorData !== $errorMessage) {
159            $expectedErrorMessage = $errorMessage !== null
160                ? "The expected error message '$errorMessage' is not equal to returned error '$errorData'"
161                : "No error message expected but received '$errorData'";
162            $this->error($expectedErrorMessage);
163        }
164
165        return $this;
166    }
167
168    /**
169     * Expect response status.
170     *
171     * @param string|null $status Expected status.
172     *
173     * @return Response
174     */
175    public function expectStatus(string|null $status): Response {
176        $headers = $this->getHeaders();
177        if (is_null($status) && !isset($headers['status'])) {
178            return $this;
179        }
180        if (!is_null($status) && !isset($headers['status'])) {
181            $this->error('Status is expected but not supplied');
182        } elseif ($status !== $headers['status']) {
183            $statusMessage = $status === null ? "expected not to be set": "expected to be $status";
184            $this->error("Status is $statusMessage but the actual value is {$headers['status']}");
185        }
186        return $this;
187    }
188
189    /**
190     * Expect response status not to be set.
191     *
192     * @return Response
193     */
194    public function expectNoStatus(): Response {
195        return $this->expectStatus(null);
196    }
197
198    /**
199     * Expect no error in the response.
200     *
201     * @return Response
202     */
203    public function expectNoError(): Response
204    {
205        return $this->expectError(null);
206    }
207
208    /**
209     * Get response body.
210     *
211     * @param string $contentType Expect body to have specified content type.
212     *
213     * @return string|null
214     */
215    public function getBody(string $contentType = 'text/html'): ?string
216    {
217        if ($this->checkIfValid() && $this->checkDefaultHeaders($contentType)) {
218            return $this->rawBody;
219        }
220
221        return null;
222    }
223
224    /**
225     * Print raw body.
226     *
227     * @param string $contentType Expect body to have specified content type.
228     */
229    public function dumpBody(string $contentType = 'text/html')
230    {
231        var_dump($this->getBody($contentType));
232    }
233
234    /**
235     * Print raw body.
236     *
237     * @param string $contentType Expect body to have specified content type.
238     */
239    public function printBody(string $contentType = 'text/html')
240    {
241        echo $this->getBody($contentType) . "\n";
242    }
243
244    /**
245     * Debug response output
246     */
247    public function debugOutput()
248    {
249        echo ">>> Response\n";
250        echo "----------------- OUT -----------------\n";
251        echo $this->data['out_response'] . "\n";
252        echo "----------------- ERR -----------------\n";
253        echo $this->data['err_response'] . "\n";
254        echo "---------------------------------------\n\n";
255    }
256
257    /**
258     * @return string|null
259     */
260    public function getErrorData(): ?string
261    {
262        return $this->data['err_response'];
263    }
264
265    /**
266     * Check if the response is valid and if not emit error message
267     *
268     * @return bool
269     */
270    private function checkIfValid(): bool
271    {
272        if ($this->isValid()) {
273            return true;
274        }
275
276        if ( ! $this->expectInvalid) {
277            $this->error("The response is invalid: $this->rawData");
278        }
279
280        return false;
281    }
282
283    /**
284     * Check default headers that should be present.
285     *
286     * @param string $contentType
287     *
288     * @return bool
289     */
290    private function checkDefaultHeaders($contentType): bool
291    {
292        // check default headers
293        return (
294            ( ! ini_get('expose_php') || $this->checkHeader('X-Powered-By', '|^PHP/8|', true)) &&
295            $this->checkHeader('Content-type', '|^' . $contentType . '(;\s?charset=\w+)?|', true)
296        );
297    }
298
299    /**
300     * Check a specified header.
301     *
302     * @param string $name     Header name.
303     * @param string $value    Header value.
304     * @param bool   $useRegex Whether value is regular expression.
305     *
306     * @return bool
307     */
308    private function checkHeader(string $name, string $value, $useRegex = false): bool
309    {
310        $lcName  = strtolower($name);
311        $headers = $this->getHeaders();
312        if ( ! isset($headers[$lcName])) {
313            return $this->error("The header $name is not present");
314        }
315        $header = $headers[$lcName];
316
317        if ( ! $useRegex) {
318            if ($header === $value) {
319                return true;
320            }
321
322            return $this->error("The header $name value '$header' is not the same as '$value'");
323        }
324
325        if ( ! preg_match($value, $header)) {
326            return $this->error("The header $name value '$header' does not match RegExp '$value'");
327        }
328
329        return true;
330    }
331
332    /**
333     * Get all headers.
334     *
335     * @return array|null
336     */
337    private function getHeaders(): ?array
338    {
339        if ( ! $this->isValid()) {
340            return null;
341        }
342
343        if (is_array($this->headers)) {
344            return $this->headers;
345        }
346
347        $headerRows = explode("\r\n", $this->rawHeaders);
348        $headers    = [];
349        foreach ($headerRows as $headerRow) {
350            $colonPosition = strpos($headerRow, ':');
351            if ($colonPosition === false) {
352                $this->error("Invalid header row (no colon): $headerRow");
353            }
354            $headers[strtolower(substr($headerRow, 0, $colonPosition))] = trim(
355                substr($headerRow, $colonPosition + 1)
356            );
357        }
358
359        return ($this->headers = $headers);
360    }
361
362    /**
363     * @return bool
364     */
365    private function isValid()
366    {
367        if ($this->valid === null) {
368            $this->processData();
369        }
370
371        return $this->valid;
372    }
373
374    /**
375     * Process data and set validity and raw data
376     */
377    private function processData()
378    {
379        $this->rawData = $this->data['out_response'];
380        $this->valid   = (
381            ! is_null($this->rawData) &&
382            strpos($this->rawData, self::HEADER_SEPARATOR)
383        );
384        if ($this->valid) {
385            list ($this->rawHeaders, $this->rawBody) = array_map(
386                'trim',
387                explode(self::HEADER_SEPARATOR, $this->rawData)
388            );
389        }
390    }
391
392    /**
393     * Emit error message
394     *
395     * @param string $message
396     *
397     * @return bool
398     */
399    private function error($message): bool
400    {
401        echo "ERROR: $message\n";
402
403        return false;
404    }
405}
406