xref: /PHP-8.2/sapi/fpm/tests/response.inc (revision 62682cbd)
1<?php
2
3namespace FPM;
4
5abstract class BaseResponse
6{
7    /**
8     * Tester instance
9     * @var Tester
10     */
11    private Tester $tester;
12
13    /**
14     * @var bool
15     */
16    protected bool $debugOutputted = false;
17
18    /**
19     * @param Tester $tester
20     */
21    public function __construct(Tester $tester)
22    {
23        $this->tester = $tester;
24    }
25
26    /**
27     * Debug response output.
28     *
29     * @return void
30     */
31    abstract function debugOutput(): void;
32
33    /**
34     * Emit error message
35     *
36     * @param string $message
37     * @param bool   $throw
38     *
39     * @return bool
40     * @throws \Exception
41     */
42    protected function error(string $message, bool $throw = false): bool
43    {
44        $errorMessage = "ERROR: $message\n";
45        if ($throw) {
46            throw new \Exception($errorMessage);
47        }
48        if ( ! $this->debugOutputted) {
49            $this->debugOutput();
50        }
51        echo $errorMessage;
52
53        $this->tester->printLogs();
54
55        return false;
56    }
57}
58
59class Response extends BaseResponse
60{
61    const HEADER_SEPARATOR = "\r\n\r\n";
62
63    /**
64     * @var array
65     */
66    private array $data;
67
68    /**
69     * @var string
70     */
71    private $rawData;
72
73    /**
74     * @var string
75     */
76    private $rawHeaders;
77
78    /**
79     * @var string
80     */
81    private $rawBody;
82
83    /**
84     * @var array
85     */
86    private $headers;
87
88    /**
89     * @var bool
90     */
91    private $valid;
92
93    /**
94     * @var bool
95     */
96    private bool $expectInvalid;
97
98    /**
99     * @param Tester            $tester
100     * @param string|array|null $data
101     * @param bool              $expectInvalid
102     */
103    public function __construct(Tester $tester, $data = null, bool $expectInvalid = false)
104    {
105        parent::__construct($tester);
106
107        if ( ! is_array($data)) {
108            $data = [
109                'response'     => $data,
110                'err_response' => null,
111                'out_response' => $data,
112            ];
113        }
114
115        $this->data          = $data;
116        $this->expectInvalid = $expectInvalid;
117    }
118
119    /**
120     * @param mixed  $body
121     * @param string $contentType
122     *
123     * @return Response
124     */
125    public function expectBody($body, $contentType = 'text/html')
126    {
127        if ($multiLine = is_array($body)) {
128            $body = implode("\n", $body);
129        }
130
131        if ( ! $this->checkIfValid()) {
132            $this->error('Response is invalid');
133        } elseif ( ! $this->checkDefaultHeaders($contentType)) {
134            $this->error('Response default headers not found');
135        } elseif ($body !== $this->rawBody) {
136            if ($multiLine) {
137                $this->error(
138                    "==> The expected body:\n$body\n" .
139                    "==> does not match the actual body:\n$this->rawBody"
140                );
141            } else {
142                $this->error(
143                    "The expected body '$body' does not match actual body '$this->rawBody'"
144                );
145            }
146        }
147
148        return $this;
149    }
150
151    /**
152     * Expect that one of the processes in json status process list has a field with value that
153     * matches the supplied pattern.
154     *
155     * @param string $fieldName
156     * @param string $pattern
157     *
158     * @return Response
159     */
160    public function expectJsonBodyPatternForStatusProcessField(string $fieldName, string $pattern): Response
161    {
162        $rawData = $this->getBody('application/json');
163        $data = json_decode($rawData, true);
164        if (empty($data['processes']) || !is_array($data['processes'])) {
165            $this->error(
166                "The body data is not a valid status json containing processes field '$rawData'"
167            );
168        }
169        foreach ($data['processes'] as $process) {
170            if (preg_match('|' . $pattern . '|', $process[$fieldName]) !== false) {
171                return $this;
172            }
173        }
174
175        $this->error(
176            "No field $fieldName matched pattern $pattern for any process in status data '$rawData'"
177        );
178
179        return $this;
180    }
181
182    /**
183     * @return Response
184     */
185    public function expectEmptyBody()
186    {
187        return $this->expectBody('');
188    }
189
190    /**
191     * Expect header in the response.
192     *
193     * @param string $name  Header name.
194     * @param string $value Header value.
195     *
196     * @return Response
197     */
198    public function expectHeader($name, $value): Response
199    {
200        $this->checkHeader($name, $value);
201
202        return $this;
203    }
204
205    /**
206     * Expect error in the response.
207     *
208     * @param string|null $errorMessage Expected error message.
209     *
210     * @return Response
211     */
212    public function expectError($errorMessage): Response
213    {
214        $errorData = $this->getErrorData();
215        if ($errorData !== $errorMessage) {
216            $expectedErrorMessage = $errorMessage !== null
217                ? "The expected error message '$errorMessage' is not equal to returned error '$errorData'"
218                : "No error message expected but received '$errorData'";
219            $this->error($expectedErrorMessage);
220        }
221
222        return $this;
223    }
224
225    /**
226     * Expect error pattern in the response.
227     *
228     * @param string $errorMessagePattern Expected error message RegExp patter.
229     *
230     * @return Response
231     */
232    public function expectErrorPattern(string $errorMessagePattern): Response
233    {
234        $errorData = $this->getErrorData();
235        if (preg_match($errorMessagePattern, $errorData) === 0) {
236            $this->error(
237                "The expected error pattern $errorMessagePattern does not match the returned error '$errorData'"
238            );
239            $this->debugOutput();
240        }
241
242        return $this;
243    }
244
245    /**
246     * Expect response status.
247     *
248     * @param string|null $status Expected status.
249     *
250     * @return Response
251     */
252    public function expectStatus(string|null $status): Response {
253        $headers = $this->getHeaders();
254        if (is_null($status) && !isset($headers['status'])) {
255            return $this;
256        }
257        if (!is_null($status) && !isset($headers['status'])) {
258            $this->error('Status is expected but not supplied');
259        } elseif ($status !== $headers['status']) {
260            $statusMessage = $status === null ? "expected not to be set": "expected to be $status";
261            $this->error("Status is $statusMessage but the actual value is {$headers['status']}");
262        }
263
264        return $this;
265    }
266
267    /**
268     * Expect response status not to be set.
269     *
270     * @return Response
271     */
272    public function expectNoStatus(): Response {
273        return $this->expectStatus(null);
274    }
275
276    /**
277     * Expect no error in the response.
278     *
279     * @return Response
280     */
281    public function expectNoError(): Response
282    {
283        return $this->expectError(null);
284    }
285
286    /**
287     * Get response body.
288     *
289     * @param string $contentType Expect body to have specified content type.
290     *
291     * @return string|null
292     */
293    public function getBody(string $contentType = 'text/html'): ?string
294    {
295        if ($this->checkIfValid() && $this->checkDefaultHeaders($contentType)) {
296            return $this->rawBody;
297        }
298
299        return null;
300    }
301
302    /**
303     * Print raw body.
304     *
305     * @param string $contentType Expect body to have specified content type.
306     */
307    public function dumpBody(string $contentType = 'text/html')
308    {
309        var_dump($this->getBody($contentType));
310    }
311
312    /**
313     * Print raw body.
314     *
315     * @param string $contentType Expect body to have specified content type.
316     */
317    public function printBody(string $contentType = 'text/html')
318    {
319        echo $this->getBody($contentType) . "\n";
320    }
321
322    /**
323     * Debug response output
324     */
325    public function debugOutput(): void
326    {
327        echo ">>> Response\n";
328        echo "----------------- OUT -----------------\n";
329        echo $this->data['out_response'] . "\n";
330        echo "----------------- ERR -----------------\n";
331        echo $this->data['err_response'] . "\n";
332        echo "---------------------------------------\n\n";
333
334        $this->debugOutputted = true;
335    }
336
337    /**
338     * @return string|null
339     */
340    public function getErrorData(): ?string
341    {
342        return $this->data['err_response'];
343    }
344
345    /**
346     * Check if the response is valid and if not emit error message
347     *
348     * @return bool
349     */
350    private function checkIfValid(): bool
351    {
352        if ($this->isValid()) {
353            return true;
354        }
355
356        if ( ! $this->expectInvalid) {
357            $this->error("The response is invalid: $this->rawData");
358        }
359
360        return false;
361    }
362
363    /**
364     * Check default headers that should be present.
365     *
366     * @param string $contentType
367     *
368     * @return bool
369     */
370    private function checkDefaultHeaders($contentType): bool
371    {
372        // check default headers
373        return (
374            ( ! ini_get('expose_php') || $this->checkHeader('X-Powered-By', '|^PHP/8|', true)) &&
375            $this->checkHeader('Content-type', '|^' . $contentType . '(;\s?charset=\w+)?|', true)
376        );
377    }
378
379    /**
380     * Check a specified header.
381     *
382     * @param string $name     Header name.
383     * @param string $value    Header value.
384     * @param bool   $useRegex Whether value is regular expression.
385     *
386     * @return bool
387     */
388    private function checkHeader(string $name, string $value, $useRegex = false): bool
389    {
390        $lcName  = strtolower($name);
391        $headers = $this->getHeaders();
392        if ( ! isset($headers[$lcName])) {
393            return $this->error("The header $name is not present");
394        }
395        $header = $headers[$lcName];
396
397        if ( ! $useRegex) {
398            if ($header === $value) {
399                return true;
400            }
401
402            return $this->error("The header $name value '$header' is not the same as '$value'");
403        }
404
405        if ( ! preg_match($value, $header)) {
406            return $this->error("The header $name value '$header' does not match RegExp '$value'");
407        }
408
409        return true;
410    }
411
412    /**
413     * Get all headers.
414     *
415     * @return array|null
416     */
417    private function getHeaders(): ?array
418    {
419        if ( ! $this->isValid()) {
420            return null;
421        }
422
423        if (is_array($this->headers)) {
424            return $this->headers;
425        }
426
427        $headerRows = explode("\r\n", $this->rawHeaders);
428        $headers    = [];
429        foreach ($headerRows as $headerRow) {
430            $colonPosition = strpos($headerRow, ':');
431            if ($colonPosition === false) {
432                $this->error("Invalid header row (no colon): $headerRow");
433            }
434            $headers[strtolower(substr($headerRow, 0, $colonPosition))] = trim(
435                substr($headerRow, $colonPosition + 1)
436            );
437        }
438
439        return ($this->headers = $headers);
440    }
441
442    /**
443     * @return bool
444     */
445    private function isValid()
446    {
447        if ($this->valid === null) {
448            $this->processData();
449        }
450
451        return $this->valid;
452    }
453
454    /**
455     * Process data and set validity and raw data
456     */
457    private function processData()
458    {
459        $this->rawData = $this->data['out_response'];
460        $this->valid   = (
461            ! is_null($this->rawData) &&
462            strpos($this->rawData, self::HEADER_SEPARATOR)
463        );
464        if ($this->valid) {
465            list ($this->rawHeaders, $this->rawBody) = array_map(
466                'trim',
467                explode(self::HEADER_SEPARATOR, $this->rawData)
468            );
469        }
470    }
471}
472
473class ValuesResponse extends BaseResponse
474{
475    /**
476     * @var array
477     */
478    private array $values;
479
480    /**
481     * @param Tester            $tester
482     * @param string|array|null $values
483     * @throws \Exception
484     */
485    public function __construct(Tester $tester, $values = null)
486    {
487        parent::__construct($tester);
488
489        if ( ! is_array($values)) {
490            if ( ! is_null($values) ) {
491                $this->error('Invalid values supplied', true);
492            }
493            $this->values = [];
494        } else {
495            $this->values = $values;
496        }
497    }
498
499    /**
500     * Expect value.
501     *
502     * @param string $name
503     * @param mixed  $value
504     * @return ValuesResponse
505     * @throws \Exception
506     */
507    public function expectValue(string $name, $value = null)
508    {
509        if ( ! isset($this->values[$name])) {
510            $this->error("Value $name not found in values");
511        }
512        if ( ! is_null($value) && $value !== $this->values[$name]) {
513            $this->error("Value $name is {$this->values[$name]} but expected $value");
514        }
515        return $this;
516    }
517
518    /**
519     * Get values.
520     *
521     * @return array
522     */
523    public function getValues()
524    {
525        return $this->values;
526    }
527
528    /**
529     * Debug output data.
530     */
531    public function debugOutput(): void
532    {
533        echo ">>> ValuesResponse\n";
534        echo "----------------- Values -----------------\n";
535        var_dump($this->values);
536        echo "---------------------------------------\n\n";
537    }
538}
539