Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
"require": {
"php": ">=8.1 <8.4",
"ext-json": "*",
"byjg/webrequest": "^5.0"
"byjg/webrequest": "^5.0",
"byjg/xmlutil": "^5.0"
},
"require-dev": {
"phpunit/phpunit": "^9.6",
Expand Down
27 changes: 13 additions & 14 deletions phpunit.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -4,33 +4,32 @@ To change this license header, choose License Headers in Project Properties.
To change this template file, choose Tools | Templates
and open the template in the editor.
-->

<!-- see http://www.phpunit.de/wiki/Documentation -->
<phpunit bootstrap="./vendor/autoload.php"
colors="true"
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
bootstrap="./vendor/autoload.php" colors="true"
testdox="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
convertDeprecationsToExceptions="true"
stopOnFailure="false">
stopOnFailure="false"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd">

<php>
<ini name="display_errors" value="On" />
<ini name="display_startup_errors" value="On" />
<ini name="error_reporting" value="E_ALL" />
<ini name="display_errors" value="On"/>
<ini name="display_startup_errors" value="On"/>
<ini name="error_reporting" value="E_ALL"/>
</php>
<filter>
<whitelist>

<coverage>
<include>
<directory>./src</directory>
</whitelist>
</filter>
</include>
</coverage>

<testsuites>
<testsuite name="Test Suite">
<directory>./tests/</directory>
</testsuite>
</testsuites>

</phpunit>
35 changes: 24 additions & 11 deletions src/AbstractRequester.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,12 @@
use ByJG\ApiTools\Exception\PathNotFoundException;
use ByJG\ApiTools\Exception\RequiredArgumentNotFound;
use ByJG\ApiTools\Exception\StatusCodeNotMatchedException;
use ByJG\Util\Uri;
use ByJG\WebRequest\Exception\MessageException;
use ByJG\WebRequest\Exception\RequestException;
use ByJG\WebRequest\Psr7\Request;
use ByJG\Util\Uri;
use ByJG\WebRequest\Psr7\MemoryStream;
use ByJG\WebRequest\Psr7\Request;
use ByJG\XmlUtil\XmlDocument;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;

Expand Down Expand Up @@ -171,6 +172,11 @@ public function withPsr7Request(RequestInterface $requestInterface): self
return $this;
}

public function getPsr7Request(): RequestInterface
{
return $this->psr7Request;
}

public function assertResponseCode(int $code): self
{
$this->statusExpected = $code;
Expand Down Expand Up @@ -204,7 +210,7 @@ public function assertBodyContains(string $contains): self
* @throws RequiredArgumentNotFound
* @throws StatusCodeNotMatchedException
*/
public function send(): ResponseInterface
public function send(bool $matchQueryParams = true): ResponseInterface
{
// Process URI based on the OpenAPI schema
$uriSchema = new Uri($this->schema->getServerUrl());
Expand All @@ -230,21 +236,28 @@ public function send(): ResponseInterface
$this->psr7Request = $this->psr7Request->withUri($uri);

// Prepare Body to Match Against Specification
$requestBody = $this->psr7Request->getBody()->getContents();
if (!empty($requestBody)) {
$contentType = $this->psr7Request->getHeaderLine("content-type");
if (empty($contentType) || str_contains($contentType, "application/json")) {
$requestBody = json_decode($requestBody, true);
$rawBody = $this->psr7Request->getBody()->getContents();
$isXmlBody = false;
$requestBody = null;
$contentType = $this->psr7Request->getHeaderLine("content-type");
if (!empty($rawBody)) {
if (str_contains($contentType, 'application/xml') || str_contains($contentType, 'text/xml')) {
$isXmlBody = new XmlDocument($rawBody);
} elseif (empty($contentType) || str_contains($contentType, "application/json")) {
$requestBody = json_decode($rawBody, true);
} elseif (str_contains($contentType, "multipart/")) {
$requestBody = $this->parseMultiPartForm($contentType, $requestBody);
$requestBody = $this->parseMultiPartForm($contentType, $rawBody);
} else {
throw new InvalidRequestException("Cannot handle Content Type '$contentType'");
}

}

// Check if the body is the expected before request
$bodyRequestDef = $this->schema->getRequestParameters($this->psr7Request->getUri()->getPath(), $this->psr7Request->getMethod());
$bodyRequestDef->match($requestBody);
if ($isXmlBody === false) {
$bodyRequestDef = $this->schema->getRequestParameters($this->psr7Request->getUri()->getPath(), $this->psr7Request->getMethod(), $matchQueryParams ? $this->psr7Request->getUri()->getQuery() : null);
$bodyRequestDef->match($requestBody);
}

// Handle Request
$response = $this->handleRequest($this->psr7Request);
Expand Down
22 changes: 20 additions & 2 deletions src/ApiTestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use ByJG\WebRequest\Psr7\Response;
use PHPUnit\Framework\TestCase;
use Psr\Http\Message\ResponseInterface;
use Throwable;

abstract class ApiTestCase extends TestCase
{
Expand Down Expand Up @@ -116,7 +117,7 @@ protected function makeRequest(
* @throws RequiredArgumentNotFound
* @throws StatusCodeNotMatchedException
*/
public function assertRequest(AbstractRequester $request): ResponseInterface
public function assertRequest(AbstractRequester $request, bool $matchQueryParams = true): ResponseInterface
{
// Add own schema if nothing is passed.
if (!$request->hasSchema()) {
Expand All @@ -125,7 +126,7 @@ public function assertRequest(AbstractRequester $request): ResponseInterface
}

// Request based on the Swagger Request definitios
$body = $request->send();
$body = $request->send($matchQueryParams);

// Note:
// This code is only reached if to send is successful and
Expand All @@ -136,6 +137,23 @@ public function assertRequest(AbstractRequester $request): ResponseInterface
return $body;
}

public function assertRequestException(AbstractRequester $request, string $exceptionClass, string $exceptionMessage = null, bool $matchQueryParams = true): Throwable
{
try {
$this->assertRequest($request, $matchQueryParams);
} catch (Throwable $ex) {
$this->assertInstanceOf($exceptionClass, $ex);

if (!empty($exceptionMessage)) {
$this->assertStringContainsString($exceptionMessage, $ex->getMessage());
}

return $ex;
}
$this->fail("Expected exception '{$exceptionClass}' but no exception was thrown");
}


/**
* @throws GenericSwaggerException
*/
Expand Down
4 changes: 2 additions & 2 deletions src/Base/Body.php
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ protected function matchString(string $name, array $schemaArray, mixed $body, mi
}

if (!is_string($body)) {
throw new NotMatchedException("Value '" . var_export($body, true) . "' in '$name' is not string. ", $this->structure);
throw new NotMatchedException("Value '" . str_replace("\n", "", var_export($body, true)) . "' in '$name' is not string. ", $this->structure);
}

return true;
Expand Down Expand Up @@ -209,7 +209,7 @@ protected function matchTypes(string $name, mixed $schemaArray, mixed $body): ?b
}

$type = $schemaArray['type'];
$nullable = isset($schemaArray['nullable']) ? (bool)$schemaArray['nullable'] : $this->schema->isAllowNullValues();
$nullable = isset($schemaArray['nullable']) ? (bool)$schemaArray['nullable'] : ($this->allowNullValues || $this->schema->isAllowNullValues());

$validators = [
function () use ($name, $body, $type, $nullable)
Expand Down
11 changes: 11 additions & 0 deletions src/Base/Parameter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

namespace ByJG\ApiTools\Base;

class Parameter extends Body
{
public function match(mixed $body): bool
{
return $this->matchSchema($this->name, $this->structure, $body) ?? false;
}
}
54 changes: 40 additions & 14 deletions src/Base/Schema.php
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ public static function getInstance(array|string $data, bool $extraArgs = false):
* @throws NotMatchedException
* @throws PathNotFoundException
*/
public function getPathDefinition(string $path, string $method): mixed
protected function parsePathRequest(string $path, string $method, ?string $queryString = null): mixed
{
$method = strtolower($method);

Expand All @@ -81,6 +81,12 @@ public function getPathDefinition(string $path, string $method): mixed
// Try direct match
if (isset($this->jsonFile[self::SWAGGER_PATHS][$uri->getPath()])) {
if (isset($this->jsonFile[self::SWAGGER_PATHS][$uri->getPath()][$method])) {

if (!is_null($queryString)) {
parse_str($queryString, $matches);
$this->prepareToValidateArguments($uri->getPath(), $method, 'query', $matches);
}

return $this->jsonFile[self::SWAGGER_PATHS][$uri->getPath()][$method];
}
throw new HttpMethodNotFoundException("The http method '$method' not found in '$path'");
Expand All @@ -104,36 +110,56 @@ public function getPathDefinition(string $path, string $method): mixed
throw new HttpMethodNotFoundException("The http method '$method' not found in '$path'");
}

$parametersPathMethod = [];
$parametersPath = [];
$this->prepareToValidateArguments($pathItem, $method, 'path', $matches);

if (isset($pathDef[$method][self::SWAGGER_PARAMETERS])) {
$parametersPathMethod = $pathDef[$method][self::SWAGGER_PARAMETERS];
if (!is_null($queryString)) {
parse_str($queryString, $queryParsed);
$this->prepareToValidateArguments($pathItem, $method, 'query', $queryParsed);
}

if (isset($pathDef[self::SWAGGER_PARAMETERS])) {
$parametersPath = $pathDef[self::SWAGGER_PARAMETERS];
}

$this->validateArguments('path', array_merge($parametersPathMethod, $parametersPath), $matches);

return $pathDef[$method];
}
}

throw new PathNotFoundException('Path "' . $path . '" not found');
}

public function getPathDefinition(string $path, string $method): mixed
{
return $this->parsePathRequest($path, $method);
}

/**
* @throws DefinitionNotFoundException
* @throws NotMatchedException
* @throws InvalidDefinitionException
*/
protected function prepareToValidateArguments(string $path, string $method, string $parameterIn, $matches): void
{
$pathDef = $this->jsonFile[self::SWAGGER_PATHS][$path];

$parametersPathMethod = [];
$parametersPath = [];

if (isset($pathDef[$method][self::SWAGGER_PARAMETERS])) {
$parametersPathMethod = $pathDef[$method][self::SWAGGER_PARAMETERS];
}

if (isset($pathDef[self::SWAGGER_PARAMETERS])) {
$parametersPath = $pathDef[self::SWAGGER_PARAMETERS];
}

$this->validateArguments($parameterIn, array_merge($parametersPathMethod, $parametersPath), $matches);
}

/**
* @param string $path
* @param string $method
* @param int $status
* @return Body
* @throws DefinitionNotFoundException
* @throws HttpMethodNotFoundException
* @throws InvalidDefinitionException
* @throws InvalidRequestException
* @throws NotMatchedException
* @throws PathNotFoundException
*/
public function getResponseParameters(string $path, string $method, int $status): Body
Expand Down Expand Up @@ -203,7 +229,7 @@ abstract public function getDefinition($name): mixed;
* @throws NotMatchedException
* @throws PathNotFoundException
*/
abstract public function getRequestParameters(string $path, string $method): Body;
abstract public function getRequestParameters(string $path, string $method, ?string $queryString = null): Body;

/**
* @param Schema $schema
Expand Down
13 changes: 11 additions & 2 deletions src/OpenApi/OpenApiResponseBody.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,16 @@ public function match(mixed $body): bool
$definition = $this->schema->getDefinition($this->structure['$ref']);
return $this->matchSchema($this->name, $definition, $body) ?? false;
}

return $this->matchSchema($this->name, $this->structure['content'][key($this->structure['content'])]['schema'], $body) ?? false;

foreach ($this->structure['content'] as $contentType => $schema) {
if ($contentType === 'application/json') {
if (!isset($schema['schema'])) {
throw new NotMatchedException("Content type " . $contentType . " does not have schema");
}
return $this->matchSchema($this->name, $schema['schema'], $body) ?? false;
}
}

return true;
}
}
22 changes: 16 additions & 6 deletions src/OpenApi/OpenApiSchema.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace ByJG\ApiTools\OpenApi;

use ByJG\ApiTools\Base\Body;
use ByJG\ApiTools\Base\Parameter;
use ByJG\ApiTools\Base\Schema;
use ByJG\ApiTools\Exception\DefinitionNotFoundException;
use ByJG\ApiTools\Exception\InvalidDefinitionException;
Expand Down Expand Up @@ -62,6 +63,10 @@ public function getBasePath(): string
*/
protected function validateArguments(string $parameterIn, array $parameters, array $arguments): void
{
$checked = array_filter($arguments, function ($key) {
return !is_numeric($key);
}, ARRAY_FILTER_USE_KEY);

foreach ($parameters as $parameter) {
if (isset($parameter['$ref'])) {
$paramParts = explode("/", $parameter['$ref']);
Expand All @@ -77,11 +82,16 @@ protected function validateArguments(string $parameterIn, array $parameters, arr
}
$parameter = $this->jsonFile[self::SWAGGER_COMPONENTS][self::SWAGGER_PARAMETERS][$paramParts[3]];
}
if ($parameter['in'] === $parameterIn &&
$parameter['schema']['type'] === "integer"
&& filter_var($arguments[$parameter['name']], FILTER_VALIDATE_INT) === false) {
throw new NotMatchedException('Path expected an integer value');
if ($parameter['in'] === $parameterIn) {
$parameterMatch = new Parameter($this, $parameter['name'], $parameter["schema"] ?? [], !($parameter["required"] ?? false));
$parameterMatch->match($arguments[$parameter['name']] ?? null);
}

unset($checked[$parameter['name']]);
}

if (!empty($checked)) {
throw new NotMatchedException("There are parameters that are not defined in the schema: " . implode(", ", array_keys($checked)));
}
}

Expand Down Expand Up @@ -110,9 +120,9 @@ public function getDefinition($name): mixed
* @inheritDoc
* @throws InvalidRequestException
*/
public function getRequestParameters(string $path, string $method): Body
public function getRequestParameters(string $path, string $method, ?string $queryString = null): Body
{
$structure = $this->getPathDefinition($path, $method);
$structure = $this->parsePathRequest($path, $method, $queryString);

if (!isset($structure['requestBody'])) {
return new OpenApiRequestBody($this, "$method $path", []);
Expand Down
Loading