xref: /PHP-8.0/sapi/fpm/tests/response.inc (revision 8e1cef43)
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     * @return Response
96     */
97    public function expectEmptyBody()
98    {
99        return $this->expectBody('');
100    }
101
102    /**
103     * Expect header in the response.
104     *
105     * @param string $name  Header name.
106     * @param string $value Header value.
107     *
108     * @return Response
109     */
110    public function expectHeader($name, $value): Response
111    {
112        $this->checkHeader($name, $value);
113
114        return $this;
115    }
116
117    /**
118     * Expect error in the response.
119     *
120     * @param string|null $errorMessage Expected error message.
121     *
122     * @return Response
123     */
124    public function expectError($errorMessage): Response
125    {
126        $errorData = $this->getErrorData();
127        if ($errorData !== $errorMessage) {
128            $expectedErrorMessage = $errorMessage !== null
129                ? "The expected error message '$errorMessage' is not equal to returned error '$errorData'"
130                : "No error message expected but received '$errorData'";
131            $this->error($expectedErrorMessage);
132        }
133
134        return $this;
135    }
136
137    /**
138     * Expect no error in the response.
139     *
140     * @return Response
141     */
142    public function expectNoError(): Response
143    {
144        return $this->expectError(null);
145    }
146
147    /**
148     * Get response body.
149     *
150     * @param string $contentType Expect body to have specified content type.
151     *
152     * @return string|null
153     */
154    public function getBody(string $contentType = 'text/html'): ?string
155    {
156        if ($this->checkIfValid() && $this->checkDefaultHeaders($contentType)) {
157            return $this->rawBody;
158        }
159
160        return null;
161    }
162
163    /**
164     * Print raw body.
165     */
166    public function dumpBody()
167    {
168        var_dump($this->getBody());
169    }
170
171    /**
172     * Print raw body.
173     */
174    public function printBody()
175    {
176        echo $this->getBody() . "\n";
177    }
178
179    /**
180     * Debug response output
181     */
182    public function debugOutput()
183    {
184        echo ">>> Response\n";
185        echo "----------------- OUT -----------------\n";
186        echo $this->data['out_response'] . "\n";
187        echo "----------------- ERR -----------------\n";
188        echo $this->data['err_response'] . "\n";
189        echo "---------------------------------------\n\n";
190    }
191
192    /**
193     * @return string|null
194     */
195    public function getErrorData(): ?string
196    {
197        return $this->data['err_response'];
198    }
199
200    /**
201     * Check if the response is valid and if not emit error message
202     *
203     * @return bool
204     */
205    private function checkIfValid(): bool
206    {
207        if ($this->isValid()) {
208            return true;
209        }
210
211        if ( ! $this->expectInvalid) {
212            $this->error("The response is invalid: $this->rawData");
213        }
214
215        return false;
216    }
217
218    /**
219     * Check default headers that should be present.
220     *
221     * @param string $contentType
222     *
223     * @return bool
224     */
225    private function checkDefaultHeaders($contentType): bool
226    {
227        // check default headers
228        return (
229            ( ! ini_get('expose_php') || $this->checkHeader('X-Powered-By', '|^PHP/8|', true)) &&
230            $this->checkHeader('Content-type', '|^' . $contentType . '(;\s?charset=\w+)?|', true)
231        );
232    }
233
234    /**
235     * Check a specified header.
236     *
237     * @param string $name     Header name.
238     * @param string $value    Header value.
239     * @param bool   $useRegex Whether value is regular expression.
240     *
241     * @return bool
242     */
243    private function checkHeader(string $name, string $value, $useRegex = false): bool
244    {
245        $lcName  = strtolower($name);
246        $headers = $this->getHeaders();
247        if ( ! isset($headers[$lcName])) {
248            return $this->error("The header $name is not present");
249        }
250        $header = $headers[$lcName];
251
252        if ( ! $useRegex) {
253            if ($header === $value) {
254                return true;
255            }
256
257            return $this->error("The header $name value '$header' is not the same as '$value'");
258        }
259
260        if ( ! preg_match($value, $header)) {
261            return $this->error("The header $name value '$header' does not match RegExp '$value'");
262        }
263
264        return true;
265    }
266
267    /**
268     * Get all headers.
269     *
270     * @return array|null
271     */
272    private function getHeaders(): ?array
273    {
274        if ( ! $this->isValid()) {
275            return null;
276        }
277
278        if (is_array($this->headers)) {
279            return $this->headers;
280        }
281
282        $headerRows = explode("\r\n", $this->rawHeaders);
283        $headers    = [];
284        foreach ($headerRows as $headerRow) {
285            $colonPosition = strpos($headerRow, ':');
286            if ($colonPosition === false) {
287                $this->error("Invalid header row (no colon): $headerRow");
288            }
289            $headers[strtolower(substr($headerRow, 0, $colonPosition))] = trim(
290                substr($headerRow, $colonPosition + 1)
291            );
292        }
293
294        return ($this->headers = $headers);
295    }
296
297    /**
298     * @return bool
299     */
300    private function isValid()
301    {
302        if ($this->valid === null) {
303            $this->processData();
304        }
305
306        return $this->valid;
307    }
308
309    /**
310     * Process data and set validity and raw data
311     */
312    private function processData()
313    {
314        $this->rawData = $this->data['out_response'];
315        $this->valid   = (
316            ! is_null($this->rawData) &&
317            strpos($this->rawData, self::HEADER_SEPARATOR)
318        );
319        if ($this->valid) {
320            list ($this->rawHeaders, $this->rawBody) = array_map(
321                'trim',
322                explode(self::HEADER_SEPARATOR, $this->rawData)
323            );
324        }
325    }
326
327    /**
328     * Emit error message
329     *
330     * @param string $message
331     *
332     * @return bool
333     */
334    private function error($message): bool
335    {
336        echo "ERROR: $message\n";
337
338        return false;
339    }
340}
341