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