xref: /php-src/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, $useRegex = false): Response
199    {
200        $this->checkHeader($name, $value, $useRegex);
201
202        return $this;
203    }
204
205    /**
206     * @param string $name
207     * @return Response
208     */
209    public function expectNoHeader($name)
210    {
211        $this->checkNoHeader($name);
212
213        return $this;
214    }
215
216    /**
217     * Expect error in the response.
218     *
219     * @param string|null $errorMessage Expected error message.
220     *
221     * @return Response
222     */
223    public function expectError($errorMessage): Response
224    {
225        $errorData = $this->getErrorData();
226        if ($errorData !== $errorMessage) {
227            $expectedErrorMessage = $errorMessage !== null
228                ? "The expected error message '$errorMessage' is not equal to returned error '$errorData'"
229                : "No error message expected but received '$errorData'";
230            $this->error($expectedErrorMessage);
231        }
232
233        return $this;
234    }
235
236    /**
237     * Expect error pattern in the response.
238     *
239     * @param string $errorMessagePattern Expected error message RegExp pattern.
240     *
241     * @return Response
242     */
243    public function expectErrorPattern(string $errorMessagePattern): Response
244    {
245        $errorData = $this->getErrorData();
246        if (preg_match($errorMessagePattern, $errorData) === 0) {
247            $this->error(
248                "The expected error pattern $errorMessagePattern does not match the returned error '$errorData'"
249            );
250            $this->debugOutput();
251        }
252
253        return $this;
254    }
255
256    /**
257     * Expect response status.
258     *
259     * @param string|null $status Expected status.
260     *
261     * @return Response
262     */
263    public function expectStatus(string|null $status): Response {
264        $headers = $this->getHeaders();
265        if (is_null($status) && !isset($headers['status'])) {
266            return $this;
267        }
268        if (!is_null($status) && !isset($headers['status'])) {
269            $this->error('Status is expected but not supplied');
270        } elseif ($status !== $headers['status']) {
271            $statusMessage = $status === null ? "expected not to be set": "expected to be $status";
272            $this->error("Status is $statusMessage but the actual value is {$headers['status']}");
273        }
274
275        return $this;
276    }
277
278    /**
279     * Expect response status not to be set.
280     *
281     * @return Response
282     */
283    public function expectNoStatus(): Response {
284        return $this->expectStatus(null);
285    }
286
287    /**
288     * Expect no error in the response.
289     *
290     * @return Response
291     */
292    public function expectNoError(): Response
293    {
294        return $this->expectError(null);
295    }
296
297    /**
298     * Get response body.
299     *
300     * @param string $contentType Expect body to have specified content type.
301     *
302     * @return string|null
303     */
304    public function getBody(string $contentType = 'text/html'): ?string
305    {
306        if ($this->checkIfValid() && $this->checkDefaultHeaders($contentType)) {
307            return $this->rawBody;
308        }
309
310        return null;
311    }
312
313    /**
314     * Print raw body.
315     *
316     * @param string $contentType Expect body to have specified content type.
317     */
318    public function dumpBody(string $contentType = 'text/html')
319    {
320        var_dump($this->getBody($contentType));
321    }
322
323    /**
324     * Print raw body.
325     *
326     * @param string $contentType Expect body to have specified content type.
327     */
328    public function printBody(string $contentType = 'text/html')
329    {
330        echo $this->getBody($contentType) . "\n";
331    }
332
333    /**
334     * Debug response output
335     */
336    public function debugOutput(): void
337    {
338        echo ">>> Response\n";
339        echo "----------------- OUT -----------------\n";
340        echo $this->data['out_response'] . "\n";
341        echo "----------------- ERR -----------------\n";
342        echo $this->data['err_response'] . "\n";
343        echo "---------------------------------------\n\n";
344
345        $this->debugOutputted = true;
346    }
347
348    /**
349     * @return string|null
350     */
351    public function getErrorData(): ?string
352    {
353        return $this->data['err_response'];
354    }
355
356    /**
357     * Check if the response is valid and if not emit error message
358     *
359     * @return bool
360     */
361    private function checkIfValid(): bool
362    {
363        if ($this->isValid()) {
364            return true;
365        }
366
367        if ( ! $this->expectInvalid) {
368            $this->error("The response is invalid: $this->rawData");
369        }
370
371        return false;
372    }
373
374    /**
375     * Check default headers that should be present.
376     *
377     * @param string $contentType
378     *
379     * @return bool
380     */
381    private function checkDefaultHeaders($contentType): bool
382    {
383        // check default headers
384        return (
385            ( ! ini_get('expose_php') || $this->checkHeader('X-Powered-By', '|^PHP/8|', true)) &&
386            $this->checkHeader('Content-type', '|^' . $contentType . '(;\s?charset=\w+)?|', true)
387        );
388    }
389
390    /**
391     * Check a specified header.
392     *
393     * @param string $name     Header name.
394     * @param string $value    Header value.
395     * @param bool   $useRegex Whether value is regular expression.
396     *
397     * @return bool
398     */
399    private function checkHeader(string $name, string $value, $useRegex = false): bool
400    {
401        $lcName  = strtolower($name);
402        $headers = $this->getHeaders();
403        if ( ! isset($headers[$lcName])) {
404            return $this->error("The header $name is not present");
405        }
406        $header = $headers[$lcName];
407
408        if ( ! $useRegex) {
409            if ($header === $value) {
410                return true;
411            }
412
413            return $this->error("The header $name value '$header' is not the same as '$value'");
414        }
415
416        if ( ! preg_match($value, $header)) {
417            return $this->error("The header $name value '$header' does not match RegExp '$value'");
418        }
419
420        return true;
421    }
422
423    /**
424     * @param string $name
425     * @return bool
426     */
427    private function checkNoHeader(string $name)
428    {
429        $lcName = strtolower($name);
430        $headers = $this->getHeaders();
431        if (isset($headers[$lcName])) {
432            return $this->error("The header $name is present");
433        }
434
435        return true;
436    }
437
438    /**
439     * Get all headers.
440     *
441     * @return array|null
442     */
443    private function getHeaders(): ?array
444    {
445        if ( ! $this->isValid()) {
446            return null;
447        }
448
449        if (is_array($this->headers)) {
450            return $this->headers;
451        }
452
453        $headerRows = explode("\r\n", $this->rawHeaders);
454        $headers    = [];
455        foreach ($headerRows as $headerRow) {
456            $colonPosition = strpos($headerRow, ':');
457            if ($colonPosition === false) {
458                $this->error("Invalid header row (no colon): $headerRow");
459            }
460            $headers[strtolower(substr($headerRow, 0, $colonPosition))] = trim(
461                substr($headerRow, $colonPosition + 1)
462            );
463        }
464
465        return ($this->headers = $headers);
466    }
467
468    /**
469     * @return bool
470     */
471    private function isValid()
472    {
473        if ($this->valid === null) {
474            $this->processData();
475        }
476
477        return $this->valid;
478    }
479
480    /**
481     * Process data and set validity and raw data
482     */
483    private function processData()
484    {
485        $this->rawData = $this->data['out_response'];
486        $this->valid   = (
487            ! is_null($this->rawData) &&
488            strpos($this->rawData, self::HEADER_SEPARATOR)
489        );
490        if ($this->valid) {
491            list ($this->rawHeaders, $this->rawBody) = array_map(
492                'trim',
493                explode(self::HEADER_SEPARATOR, $this->rawData)
494            );
495        }
496    }
497}
498
499class ValuesResponse extends BaseResponse
500{
501    /**
502     * @var array
503     */
504    private array $values;
505
506    /**
507     * @param Tester            $tester
508     * @param string|array|null $values
509     * @throws \Exception
510     */
511    public function __construct(Tester $tester, $values = null)
512    {
513        parent::__construct($tester);
514
515        if ( ! is_array($values)) {
516            if ( ! is_null($values) ) {
517                $this->error('Invalid values supplied', true);
518            }
519            $this->values = [];
520        } else {
521            $this->values = $values;
522        }
523    }
524
525    /**
526     * Expect value.
527     *
528     * @param string $name
529     * @param mixed  $value
530     * @return ValuesResponse
531     * @throws \Exception
532     */
533    public function expectValue(string $name, $value = null)
534    {
535        if ( ! isset($this->values[$name])) {
536            $this->error("Value $name not found in values");
537        }
538        if ( ! is_null($value) && $value !== $this->values[$name]) {
539            $this->error("Value $name is {$this->values[$name]} but expected $value");
540        }
541        return $this;
542    }
543
544    /**
545     * Get values.
546     *
547     * @return array
548     */
549    public function getValues()
550    {
551        return $this->values;
552    }
553
554    /**
555     * Debug output data.
556     */
557    public function debugOutput(): void
558    {
559        echo ">>> ValuesResponse\n";
560        echo "----------------- Values -----------------\n";
561        var_dump($this->values);
562        echo "---------------------------------------\n\n";
563    }
564}
565