2022-01-07 13:45:52 +01:00
|
|
|
<?php
|
|
|
|
|
|
|
|
declare(strict_types=1);
|
|
|
|
|
|
|
|
namespace App\Test\Helper;
|
|
|
|
|
2022-01-19 15:15:49 +01:00
|
|
|
use Cake\TestSuite\IntegrationTestTrait;
|
2022-01-07 17:08:00 +01:00
|
|
|
use Cake\Http\Exception\NotImplementedException;
|
2022-01-19 15:15:49 +01:00
|
|
|
use Cake\Http\ServerRequestFactory;
|
|
|
|
use Cake\Http\ServerRequest;
|
2022-01-07 13:45:52 +01:00
|
|
|
use \League\OpenAPIValidation\PSR7\ValidatorBuilder;
|
|
|
|
use \League\OpenAPIValidation\PSR7\RequestValidator;
|
|
|
|
use \League\OpenAPIValidation\PSR7\ResponseValidator;
|
|
|
|
use \League\OpenAPIValidation\PSR7\OperationAddress;
|
2022-01-19 15:15:49 +01:00
|
|
|
use PHPUnit\Exception as PHPUnitException;
|
2022-01-07 13:45:52 +01:00
|
|
|
|
2022-01-19 15:15:49 +01:00
|
|
|
/**
|
|
|
|
* Trait ApiTestTrait
|
|
|
|
*
|
|
|
|
* @package App\Test\TestCase\Helper
|
|
|
|
*/
|
2022-01-07 13:45:52 +01:00
|
|
|
trait ApiTestTrait
|
|
|
|
{
|
2022-01-19 15:15:49 +01:00
|
|
|
use IntegrationTestTrait {
|
|
|
|
IntegrationTestTrait::_buildRequest as _buildRequestOriginal;
|
|
|
|
IntegrationTestTrait::_sendRequest as _sendRequestOriginal;
|
|
|
|
}
|
|
|
|
|
2022-01-07 13:45:52 +01:00
|
|
|
/** @var string */
|
|
|
|
protected $_authToken = '';
|
|
|
|
|
|
|
|
/** @var ValidatorBuilder */
|
2022-01-19 15:15:49 +01:00
|
|
|
private $_validator;
|
2022-01-07 13:45:52 +01:00
|
|
|
|
|
|
|
/** @var RequestValidator */
|
2022-01-19 15:15:49 +01:00
|
|
|
private $_requestValidator;
|
2022-01-07 13:45:52 +01:00
|
|
|
|
|
|
|
/** @var ResponseValidator */
|
2022-01-19 15:15:49 +01:00
|
|
|
private $_responseValidator;
|
|
|
|
|
|
|
|
/** @var ServerRequest */
|
|
|
|
protected $_psrRequest;
|
|
|
|
|
|
|
|
/* @var boolean */
|
|
|
|
protected $_skipOpenApiValidations = false;
|
2022-01-07 13:45:52 +01:00
|
|
|
|
2022-01-17 16:02:15 +01:00
|
|
|
public function setUp(): void
|
|
|
|
{
|
|
|
|
parent::setUp();
|
|
|
|
$this->initializeOpenApiValidator($_ENV['OPENAPI_SPEC'] ?? APP . '../webroot/docs/openapi.yaml');
|
|
|
|
}
|
|
|
|
|
2022-01-07 13:45:52 +01:00
|
|
|
public function setAuthToken(string $authToken): void
|
|
|
|
{
|
|
|
|
$this->_authToken = $authToken;
|
|
|
|
|
|
|
|
// somehow this is not set automatically in test environment
|
|
|
|
$_SERVER['HTTP_AUTHORIZATION'] = $authToken;
|
|
|
|
|
|
|
|
$this->configRequest([
|
|
|
|
'headers' => [
|
|
|
|
'Accept' => 'application/json',
|
2022-01-19 15:15:49 +01:00
|
|
|
'Authorization' => $this->_authToken,
|
|
|
|
'Content-Type' => 'application/json'
|
2022-01-07 13:45:52 +01:00
|
|
|
]
|
|
|
|
]);
|
|
|
|
}
|
|
|
|
|
2022-01-19 15:15:49 +01:00
|
|
|
/**
|
|
|
|
* Skip OpenAPI validations.
|
|
|
|
*
|
|
|
|
* @return void
|
|
|
|
*/
|
|
|
|
public function skipOpenApiValidations(): void
|
|
|
|
{
|
|
|
|
$this->_skipOpenApiValidations = true;
|
|
|
|
}
|
|
|
|
|
2022-01-07 17:08:00 +01:00
|
|
|
public function assertResponseContainsArray(array $expected): void
|
|
|
|
{
|
|
|
|
$responseArray = json_decode((string)$this->_response->getBody(), true);
|
|
|
|
throw new NotImplementedException('TODO: see codeception seeResponseContainsJson()');
|
|
|
|
}
|
|
|
|
|
2022-01-07 13:45:52 +01:00
|
|
|
/**
|
|
|
|
* Parse the OpenAPI specification and create a validator
|
|
|
|
*
|
|
|
|
* @param string $specFile
|
|
|
|
* @return void
|
|
|
|
*/
|
2022-01-17 16:02:15 +01:00
|
|
|
public function initializeOpenApiValidator(string $specFile): void
|
2022-01-07 13:45:52 +01:00
|
|
|
{
|
2022-01-19 15:15:49 +01:00
|
|
|
$this->_validator = (new ValidatorBuilder)->fromYamlFile($specFile);
|
|
|
|
$this->_requestValidator = $this->_validator->getRequestValidator();
|
|
|
|
$this->_responseValidator = $this->_validator->getResponseValidator();
|
2022-01-07 13:45:52 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Validates the API request against the OpenAPI spec
|
|
|
|
*
|
|
|
|
* @return void
|
|
|
|
*/
|
2022-01-19 15:15:49 +01:00
|
|
|
public function assertRequestMatchesOpenApiSpec(): void
|
2022-01-07 13:45:52 +01:00
|
|
|
{
|
2022-01-19 15:15:49 +01:00
|
|
|
$this->_requestValidator->validate($this->_psrRequest);
|
2022-01-07 13:45:52 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Validates the API response against the OpenAPI spec
|
|
|
|
*
|
|
|
|
* @param string $path The path to the API endpoint
|
|
|
|
* @param string $method The HTTP method used to call the endpoint
|
|
|
|
* @return void
|
|
|
|
*/
|
2022-01-10 11:58:52 +01:00
|
|
|
public function assertResponseMatchesOpenApiSpec(string $endpoint, string $method = 'get'): void
|
2022-01-07 13:45:52 +01:00
|
|
|
{
|
|
|
|
$address = new OperationAddress($endpoint, $method);
|
2022-01-19 15:15:49 +01:00
|
|
|
$this->_responseValidator->validate($address, $this->_response);
|
2022-01-07 13:45:52 +01:00
|
|
|
}
|
2022-01-10 11:58:52 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Validates a record exists in the database
|
|
|
|
*
|
|
|
|
* @param string $table The table name
|
|
|
|
* @param array $conditions The conditions to check
|
|
|
|
* @return void
|
|
|
|
* @throws \Exception
|
|
|
|
* @throws \Cake\Datasource\Exception\RecordNotFoundException
|
|
|
|
*
|
|
|
|
* @see https://book.cakephp.org/4/en/orm-query-builder.html
|
|
|
|
*/
|
|
|
|
public function assertDbRecordExists(string $table, array $conditions): void
|
|
|
|
{
|
|
|
|
$record = $this->getTableLocator()->get($table)->find()->where($conditions)->first();
|
|
|
|
if (!$record) {
|
|
|
|
throw new \PHPUnit\Framework\AssertionFailedError("Record not found in table '$table' with conditions: " . json_encode($conditions));
|
|
|
|
}
|
|
|
|
$this->assertNotEmpty($record);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2022-01-19 15:15:49 +01:00
|
|
|
* Validates a record do not exists in the database
|
2022-01-10 11:58:52 +01:00
|
|
|
*
|
|
|
|
* @param string $table The table name
|
|
|
|
* @param array $conditions The conditions to check
|
|
|
|
* @return void
|
|
|
|
* @throws \Exception
|
|
|
|
* @throws \Cake\Datasource\Exception\RecordNotFoundException
|
|
|
|
*
|
|
|
|
* @see https://book.cakephp.org/4/en/orm-query-builder.html
|
|
|
|
*/
|
|
|
|
public function assertDbRecordNotExists(string $table, array $conditions): void
|
|
|
|
{
|
|
|
|
$record = $this->getTableLocator()->get($table)->find()->where($conditions)->first();
|
|
|
|
if ($record) {
|
|
|
|
throw new \PHPUnit\Framework\AssertionFailedError("Record found in table '$table' with conditions: " . json_encode($conditions));
|
|
|
|
}
|
|
|
|
$this->assertEmpty($record);
|
|
|
|
}
|
2022-01-19 10:45:51 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Parses the response body and returns the decoded JSON
|
|
|
|
*
|
2022-01-19 15:15:49 +01:00
|
|
|
* @return array
|
2022-01-19 10:45:51 +01:00
|
|
|
* @throws \Exception
|
|
|
|
*/
|
|
|
|
public function getJsonResponseAsArray(): array
|
|
|
|
{
|
|
|
|
if ($this->_response->getHeaders()['Content-Type'][0] !== 'application/json') {
|
|
|
|
throw new \Exception('The response is not a JSON response');
|
|
|
|
}
|
|
|
|
|
|
|
|
return json_decode((string)$this->_response->getBody(), true);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Gets a database records as an array
|
|
|
|
*
|
|
|
|
* @param string $table The table name
|
|
|
|
* @param array $conditions The conditions to check
|
|
|
|
* @return array
|
|
|
|
* @throws \Cake\Datasource\Exception\RecordNotFoundException
|
|
|
|
*/
|
|
|
|
public function getRecordFromDb(string $table, array $conditions): array
|
|
|
|
{
|
|
|
|
return $this->getTableLocator()->get($table)->find()->where($conditions)->first()->toArray();
|
|
|
|
}
|
2022-01-19 15:15:49 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* This method intercepts IntegrationTestTrait::_buildRequest()
|
|
|
|
* in the quest to get a PSR-7 request object and saves it for
|
|
|
|
* later inspection, also validates it against the OpenAPI spec.
|
|
|
|
* @see \Cake\TestSuite\IntegrationTestTrait::_buildRequest()
|
|
|
|
*
|
|
|
|
* @param string $url The URL
|
|
|
|
* @param string $method The HTTP method
|
|
|
|
* @param array|string $data The request data.
|
|
|
|
* @return array The request context
|
|
|
|
*/
|
|
|
|
protected function _buildRequest(string $url, $method, $data = []): array
|
|
|
|
{
|
|
|
|
$spec = $this->_buildRequestOriginal($url, $method, $data);
|
|
|
|
|
|
|
|
$this->_psrRequest = $this->_createPsr7RequestFromSpec($spec);
|
|
|
|
|
|
|
|
// Validate request against OpenAPI spec
|
|
|
|
if (!$this->_skipOpenApiValidations) {
|
|
|
|
try {
|
|
|
|
$this->assertRequestMatchesOpenApiSpec();
|
|
|
|
} catch (\Exception $exception) {
|
|
|
|
$this->fail($exception->getMessage());
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
$this->addWarning(
|
|
|
|
sprintf(
|
|
|
|
'OpenAPI spec validations skipped for request [%s]%s.',
|
|
|
|
$this->_psrRequest->getMethod(),
|
|
|
|
$this->_psrRequest->getPath()
|
|
|
|
)
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
return $spec;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* This method intercepts IntegrationTestTrait::_buildRequest()
|
|
|
|
* and validates the response against the OpenAPI spec.
|
|
|
|
*
|
|
|
|
* @see \Cake\TestSuite\IntegrationTestTrait::_sendRequest()
|
|
|
|
*
|
|
|
|
* @param array|string $url The URL
|
|
|
|
* @param string $method The HTTP method
|
|
|
|
* @param array|string $data The request data.
|
|
|
|
* @return void
|
|
|
|
* @throws \PHPUnit\Exception|\Throwable
|
|
|
|
*/
|
|
|
|
protected function _sendRequest($url, $method, $data = []): void
|
|
|
|
{
|
|
|
|
// Adding Content-Type: application/json $this->configRequest() prevents this from happening somehow
|
|
|
|
if (in_array($method, ['POST', 'PATCH', 'PUT']) && $this->_request['headers']['Content-Type'] === 'application/json') {
|
|
|
|
$data = json_encode($data);
|
|
|
|
}
|
|
|
|
|
|
|
|
$this->_sendRequestOriginal($url, $method, $data);
|
|
|
|
|
|
|
|
// Validate response against OpenAPI spec
|
|
|
|
if (!$this->_skipOpenApiValidations) {
|
|
|
|
$this->assertResponseMatchesOpenApiSpec(
|
|
|
|
$this->_psrRequest->getPath(),
|
|
|
|
strtolower($this->_psrRequest->getMethod())
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
$this->addWarning(
|
|
|
|
sprintf(
|
|
|
|
'OpenAPI spec validations skipped for response of [%s]%s.',
|
|
|
|
$this->_psrRequest->getMethod(),
|
|
|
|
$this->_psrRequest->getPath()
|
|
|
|
)
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Create a PSR-7 request from the request spec.
|
|
|
|
* @see \Cake\TestSuite\MiddlewareDispatcher::_createRequest()
|
|
|
|
*
|
|
|
|
* @param array<string, mixed> $spec The request spec.
|
|
|
|
* @return \Cake\Http\ServerRequest
|
|
|
|
*/
|
|
|
|
private function _createPsr7RequestFromSpec(array $spec): ServerRequest
|
|
|
|
{
|
|
|
|
if (isset($spec['input'])) {
|
|
|
|
$spec['post'] = [];
|
|
|
|
$spec['environment']['CAKEPHP_INPUT'] = $spec['input'];
|
|
|
|
}
|
|
|
|
$environment = array_merge(
|
|
|
|
array_merge($_SERVER, ['REQUEST_URI' => $spec['url']]),
|
|
|
|
$spec['environment']
|
|
|
|
);
|
|
|
|
if (strpos($environment['PHP_SELF'], 'phpunit') !== false) {
|
|
|
|
$environment['PHP_SELF'] = '/';
|
|
|
|
}
|
|
|
|
return ServerRequestFactory::fromGlobals(
|
|
|
|
$environment,
|
|
|
|
$spec['query'],
|
|
|
|
$spec['post'],
|
|
|
|
$spec['cookies'],
|
|
|
|
$spec['files']
|
|
|
|
);
|
|
|
|
}
|
2022-01-07 13:45:52 +01:00
|
|
|
}
|