mirror of
https://github.com/php/php-src.git
synced 2026-03-24 16:22:37 +01:00
589 lines
14 KiB
PHP
589 lines
14 KiB
PHP
<?php
|
|
|
|
namespace FPM;
|
|
|
|
abstract class BaseResponse
|
|
{
|
|
/**
|
|
* Tester instance
|
|
* @var Tester
|
|
*/
|
|
private Tester $tester;
|
|
|
|
/**
|
|
* @var bool
|
|
*/
|
|
protected bool $debugOutputted = false;
|
|
|
|
/**
|
|
* @param Tester $tester
|
|
*/
|
|
public function __construct(Tester $tester)
|
|
{
|
|
$this->tester = $tester;
|
|
}
|
|
|
|
/**
|
|
* Debug response output.
|
|
*
|
|
* @return void
|
|
*/
|
|
abstract function debugOutput(): void;
|
|
|
|
/**
|
|
* Emit error message
|
|
*
|
|
* @param string $message
|
|
* @param bool $throw
|
|
*
|
|
* @return bool
|
|
* @throws \Exception
|
|
*/
|
|
protected function error(string $message, bool $throw = false): bool
|
|
{
|
|
$errorMessage = "ERROR: $message\n";
|
|
if ($throw) {
|
|
throw new \Exception($errorMessage);
|
|
}
|
|
if ( ! $this->debugOutputted) {
|
|
$this->debugOutput();
|
|
}
|
|
echo $errorMessage;
|
|
|
|
$this->tester->printLogs();
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
class Response extends BaseResponse
|
|
{
|
|
const HEADER_SEPARATOR = "\r\n\r\n";
|
|
|
|
/**
|
|
* @var array
|
|
*/
|
|
private array $data;
|
|
|
|
/**
|
|
* @var string
|
|
*/
|
|
private $rawData;
|
|
|
|
/**
|
|
* @var string
|
|
*/
|
|
private $rawHeaders;
|
|
|
|
/**
|
|
* @var string
|
|
*/
|
|
private $rawBody;
|
|
|
|
/**
|
|
* @var array
|
|
*/
|
|
private $headers;
|
|
|
|
/**
|
|
* @var bool
|
|
*/
|
|
private $valid;
|
|
|
|
/**
|
|
* @var bool
|
|
*/
|
|
private bool $expectInvalid;
|
|
|
|
/**
|
|
* @param Tester $tester
|
|
* @param string|array|null $data
|
|
* @param bool $expectInvalid
|
|
*/
|
|
public function __construct(Tester $tester, $data = null, bool $expectInvalid = false)
|
|
{
|
|
parent::__construct($tester);
|
|
|
|
if ( ! is_array($data)) {
|
|
$data = [
|
|
'response' => $data,
|
|
'err_response' => null,
|
|
'out_response' => $data,
|
|
];
|
|
}
|
|
|
|
$this->data = $data;
|
|
$this->expectInvalid = $expectInvalid;
|
|
}
|
|
|
|
/**
|
|
* @param mixed $body
|
|
* @param string $contentType
|
|
* @param bool $skipHeadersCheck
|
|
*
|
|
* @return Response
|
|
*/
|
|
public function expectBody($body, $contentType = 'text/html', bool $skipHeadersCheck = false)
|
|
{
|
|
if ($multiLine = is_array($body)) {
|
|
$body = implode("\n", $body);
|
|
}
|
|
|
|
if ( ! $this->checkIfValid()) {
|
|
$this->error('Response is invalid');
|
|
} elseif ( ! $skipHeadersCheck && ! $this->checkDefaultHeaders($contentType)) {
|
|
$this->error('Response default headers not found');
|
|
} elseif ($body !== $this->rawBody) {
|
|
if ($multiLine) {
|
|
$this->error(
|
|
"==> The expected body:\n$body\n" .
|
|
"==> does not match the actual body:\n$this->rawBody"
|
|
);
|
|
} else {
|
|
$this->error(
|
|
"The expected body '$body' does not match actual body '$this->rawBody'"
|
|
);
|
|
}
|
|
}
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Expect status field with value that matches the supplied pattern.
|
|
*
|
|
* @param string $fieldName
|
|
* @param string $pattern
|
|
*
|
|
* @return Response
|
|
*/
|
|
public function expectJsonBodyPatternForStatusField(string $fieldName, string $pattern): Response
|
|
{
|
|
$rawData = $this->getBody('application/json');
|
|
$data = json_decode($rawData, true);
|
|
if (preg_match('|' . $pattern . '|', $data[$fieldName]) > 0) {
|
|
return $this;
|
|
}
|
|
|
|
$this->error(
|
|
"Field $fieldName did not match pattern $pattern in status data '$rawData'"
|
|
);
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Expect that one of the processes in json status process list has a field with value that
|
|
* matches the supplied pattern.
|
|
*
|
|
* @param string $fieldName
|
|
* @param string $pattern
|
|
*
|
|
* @return Response
|
|
*/
|
|
public function expectJsonBodyPatternForStatusProcessField(string $fieldName, string $pattern): Response
|
|
{
|
|
$rawData = $this->getBody('application/json');
|
|
$data = json_decode($rawData, true);
|
|
if (empty($data['processes']) || !is_array($data['processes'])) {
|
|
$this->error(
|
|
"The body data is not a valid status json containing processes field '$rawData'"
|
|
);
|
|
}
|
|
foreach ($data['processes'] as $process) {
|
|
if (preg_match('|' . $pattern . '|', $process[$fieldName]) > 0) {
|
|
return $this;
|
|
}
|
|
}
|
|
|
|
$this->error(
|
|
"No field $fieldName matched pattern $pattern for any process in status data '$rawData'"
|
|
);
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* @return Response
|
|
*/
|
|
public function expectEmptyBody()
|
|
{
|
|
return $this->expectBody('');
|
|
}
|
|
|
|
/**
|
|
* Expect header in the response.
|
|
*
|
|
* @param string $name Header name.
|
|
* @param string $value Header value.
|
|
*
|
|
* @return Response
|
|
*/
|
|
public function expectHeader($name, $value, $useRegex = false): Response
|
|
{
|
|
$this->checkHeader($name, $value, $useRegex);
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* @param string $name
|
|
* @return Response
|
|
*/
|
|
public function expectNoHeader($name)
|
|
{
|
|
$this->checkNoHeader($name);
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Expect error in the response.
|
|
*
|
|
* @param string|null $errorMessage Expected error message.
|
|
*
|
|
* @return Response
|
|
*/
|
|
public function expectError($errorMessage): Response
|
|
{
|
|
$errorData = $this->getErrorData();
|
|
if ($errorData !== $errorMessage) {
|
|
$expectedErrorMessage = $errorMessage !== null
|
|
? "The expected error message '$errorMessage' is not equal to returned error '$errorData'"
|
|
: "No error message expected but received '$errorData'";
|
|
$this->error($expectedErrorMessage);
|
|
}
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Expect error pattern in the response.
|
|
*
|
|
* @param string $errorMessagePattern Expected error message RegExp pattern.
|
|
*
|
|
* @return Response
|
|
*/
|
|
public function expectErrorPattern(string $errorMessagePattern): Response
|
|
{
|
|
$errorData = $this->getErrorData();
|
|
if (preg_match($errorMessagePattern, $errorData) === 0) {
|
|
$this->error(
|
|
"The expected error pattern $errorMessagePattern does not match the returned error '$errorData'"
|
|
);
|
|
$this->debugOutput();
|
|
}
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Expect response status.
|
|
*
|
|
* @param string|null $status Expected status.
|
|
*
|
|
* @return Response
|
|
*/
|
|
public function expectStatus(string|null $status): Response {
|
|
$headers = $this->getHeaders();
|
|
if (is_null($status) && !isset($headers['status'])) {
|
|
return $this;
|
|
}
|
|
if (!is_null($status) && !isset($headers['status'])) {
|
|
$this->error('Status is expected but not supplied');
|
|
} elseif ($status !== $headers['status']) {
|
|
$statusMessage = $status === null ? "expected not to be set": "expected to be $status";
|
|
$this->error("Status is $statusMessage but the actual value is {$headers['status']}");
|
|
}
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Expect response status not to be set.
|
|
*
|
|
* @return Response
|
|
*/
|
|
public function expectNoStatus(): Response {
|
|
return $this->expectStatus(null);
|
|
}
|
|
|
|
/**
|
|
* Expect no error in the response.
|
|
*
|
|
* @return Response
|
|
*/
|
|
public function expectNoError(): Response
|
|
{
|
|
return $this->expectError(null);
|
|
}
|
|
|
|
/**
|
|
* Get response body.
|
|
*
|
|
* @param string $contentType Expect body to have specified content type.
|
|
*
|
|
* @return string|null
|
|
*/
|
|
public function getBody(string $contentType = 'text/html'): ?string
|
|
{
|
|
if ($this->checkIfValid() && $this->checkDefaultHeaders($contentType)) {
|
|
return $this->rawBody;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Print raw body.
|
|
*
|
|
* @param string $contentType Expect body to have specified content type.
|
|
*/
|
|
public function dumpBody(string $contentType = 'text/html')
|
|
{
|
|
var_dump($this->getBody($contentType));
|
|
}
|
|
|
|
/**
|
|
* Print raw body.
|
|
*
|
|
* @param string $contentType Expect body to have specified content type.
|
|
*/
|
|
public function printBody(string $contentType = 'text/html')
|
|
{
|
|
echo $this->getBody($contentType) . "\n";
|
|
}
|
|
|
|
/**
|
|
* Debug response output
|
|
*/
|
|
public function debugOutput(): void
|
|
{
|
|
echo ">>> Response\n";
|
|
echo "----------------- OUT -----------------\n";
|
|
echo $this->data['out_response'] . "\n";
|
|
echo "----------------- ERR -----------------\n";
|
|
echo $this->data['err_response'] . "\n";
|
|
echo "---------------------------------------\n\n";
|
|
|
|
$this->debugOutputted = true;
|
|
}
|
|
|
|
/**
|
|
* @return string|null
|
|
*/
|
|
public function getErrorData(): ?string
|
|
{
|
|
return $this->data['err_response'];
|
|
}
|
|
|
|
/**
|
|
* Check if the response is valid and if not emit error message
|
|
*
|
|
* @return bool
|
|
*/
|
|
private function checkIfValid(): bool
|
|
{
|
|
if ($this->isValid()) {
|
|
return true;
|
|
}
|
|
|
|
if ( ! $this->expectInvalid) {
|
|
$this->error("The response is invalid: $this->rawData");
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Check default headers that should be present.
|
|
*
|
|
* @param string $contentType
|
|
*
|
|
* @return bool
|
|
*/
|
|
private function checkDefaultHeaders($contentType): bool
|
|
{
|
|
// check default headers
|
|
return (
|
|
( ! ini_get('expose_php') || $this->checkHeader('X-Powered-By', '|^PHP/8|', true)) &&
|
|
$this->checkHeader('Content-type', '|^' . $contentType . '(;\s?charset=\w+)?|', true)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Check a specified header.
|
|
*
|
|
* @param string $name Header name.
|
|
* @param string $value Header value.
|
|
* @param bool $useRegex Whether value is regular expression.
|
|
*
|
|
* @return bool
|
|
*/
|
|
private function checkHeader(string $name, string $value, $useRegex = false): bool
|
|
{
|
|
$lcName = strtolower($name);
|
|
$headers = $this->getHeaders();
|
|
if ( ! isset($headers[$lcName])) {
|
|
return $this->error("The header $name is not present");
|
|
}
|
|
$header = $headers[$lcName];
|
|
|
|
if ( ! $useRegex) {
|
|
if ($header === $value) {
|
|
return true;
|
|
}
|
|
|
|
return $this->error("The header $name value '$header' is not the same as '$value'");
|
|
}
|
|
|
|
if ( ! preg_match($value, $header)) {
|
|
return $this->error("The header $name value '$header' does not match RegExp '$value'");
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @param string $name
|
|
* @return bool
|
|
*/
|
|
private function checkNoHeader(string $name)
|
|
{
|
|
$lcName = strtolower($name);
|
|
$headers = $this->getHeaders();
|
|
if (isset($headers[$lcName])) {
|
|
return $this->error("The header $name is present");
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Get all headers.
|
|
*
|
|
* @return array|null
|
|
*/
|
|
private function getHeaders(): ?array
|
|
{
|
|
if ( ! $this->isValid()) {
|
|
return null;
|
|
}
|
|
|
|
if (is_array($this->headers)) {
|
|
return $this->headers;
|
|
}
|
|
|
|
$headerRows = explode("\r\n", $this->rawHeaders);
|
|
$headers = [];
|
|
foreach ($headerRows as $headerRow) {
|
|
$colonPosition = strpos($headerRow, ':');
|
|
if ($colonPosition === false) {
|
|
$this->error("Invalid header row (no colon): $headerRow");
|
|
}
|
|
$headers[strtolower(substr($headerRow, 0, $colonPosition))] = trim(
|
|
substr($headerRow, $colonPosition + 1)
|
|
);
|
|
}
|
|
|
|
return ($this->headers = $headers);
|
|
}
|
|
|
|
/**
|
|
* @return bool
|
|
*/
|
|
private function isValid()
|
|
{
|
|
if ($this->valid === null) {
|
|
$this->processData();
|
|
}
|
|
|
|
return $this->valid;
|
|
}
|
|
|
|
/**
|
|
* Process data and set validity and raw data
|
|
*/
|
|
private function processData()
|
|
{
|
|
$this->rawData = $this->data['out_response'];
|
|
$this->valid = (
|
|
! is_null($this->rawData) &&
|
|
strpos($this->rawData, self::HEADER_SEPARATOR)
|
|
);
|
|
if ($this->valid) {
|
|
list ($this->rawHeaders, $this->rawBody) = array_map(
|
|
'trim',
|
|
explode(self::HEADER_SEPARATOR, $this->rawData)
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
class ValuesResponse extends BaseResponse
|
|
{
|
|
/**
|
|
* @var array
|
|
*/
|
|
private array $values;
|
|
|
|
/**
|
|
* @param Tester $tester
|
|
* @param string|array|null $values
|
|
* @throws \Exception
|
|
*/
|
|
public function __construct(Tester $tester, $values = null)
|
|
{
|
|
parent::__construct($tester);
|
|
|
|
if ( ! is_array($values)) {
|
|
if ( ! is_null($values) ) {
|
|
$this->error('Invalid values supplied', true);
|
|
}
|
|
$this->values = [];
|
|
} else {
|
|
$this->values = $values;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Expect value.
|
|
*
|
|
* @param string $name
|
|
* @param mixed $value
|
|
* @return ValuesResponse
|
|
* @throws \Exception
|
|
*/
|
|
public function expectValue(string $name, $value = null)
|
|
{
|
|
if ( ! isset($this->values[$name])) {
|
|
$this->error("Value $name not found in values");
|
|
}
|
|
if ( ! is_null($value) && $value !== $this->values[$name]) {
|
|
$this->error("Value $name is {$this->values[$name]} but expected $value");
|
|
}
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Get values.
|
|
*
|
|
* @return array
|
|
*/
|
|
public function getValues()
|
|
{
|
|
return $this->values;
|
|
}
|
|
|
|
/**
|
|
* Debug output data.
|
|
*/
|
|
public function debugOutput(): void
|
|
{
|
|
echo ">>> ValuesResponse\n";
|
|
echo "----------------- Values -----------------\n";
|
|
var_dump($this->values);
|
|
echo "---------------------------------------\n\n";
|
|
}
|
|
}
|