[Slim4] Add Data Mocker middleware (#4978)

* [Slim4] Store response schemas

* [Slim4] Add Data Mocker middleware

* [Slim4] Enhance Slim router

* [Slim4] Enhance config

* [Slim4] Fix data format key in object mocking

* [Slim4] Add tests for Data Mocker middleware

* [Slim4] Add Mock feature documentation

* [Slim4] Refresh samples
This commit is contained in:
Yuriy Belenko
2020-03-03 18:53:57 +03:00
committed by GitHub
parent 440aaa4ca3
commit 39aeb4a8ae
13 changed files with 2058 additions and 12 deletions

View File

@@ -113,6 +113,9 @@ public class PhpSlim4ServerCodegen extends PhpSlimServerCodegen {
additionalProperties.put("interfacesSrcPath", "./" + toSrcPath(interfacesPackage, srcBasePath));
additionalProperties.put("interfacesTestPath", "./" + toSrcPath(interfacesPackage, testBasePath));
// external docs folder
additionalProperties.put("docsBasePath", "./" + docsBasePath);
if (additionalProperties.containsKey(PSR7_IMPLEMENTATION)) {
this.setPsr7Implementation((String) additionalProperties.get(PSR7_IMPLEMENTATION));
}
@@ -150,6 +153,9 @@ public class PhpSlim4ServerCodegen extends PhpSlimServerCodegen {
supportingFiles.add(new SupportingFile("openapi_data_mocker_interface.mustache", toSrcPath(mockPackage, srcBasePath), toInterfaceName("OpenApiDataMocker") + ".php"));
supportingFiles.add(new SupportingFile("openapi_data_mocker.mustache", toSrcPath(mockPackage, srcBasePath), "OpenApiDataMocker.php"));
supportingFiles.add(new SupportingFile("openapi_data_mocker_test.mustache", toSrcPath(mockPackage, testBasePath), "OpenApiDataMockerTest.php"));
supportingFiles.add(new SupportingFile("openapi_data_mocker_middleware.mustache", toSrcPath(mockPackage, srcBasePath), "OpenApiDataMockerMiddleware.php"));
supportingFiles.add(new SupportingFile("openapi_data_mocker_middleware_test.mustache", toSrcPath(mockPackage, testBasePath), "OpenApiDataMockerMiddlewareTest.php"));
supportingFiles.add(new SupportingFile("mock_server.mustache", docsBasePath, "MockServer.md"));
// traits of ported utils
supportingFiles.add(new SupportingFile("string_utils_trait.mustache", toSrcPath(utilsPackage, srcBasePath), toTraitName("StringUtils") + ".php"));

View File

@@ -57,6 +57,8 @@ Command | Target
`$ composer test` | All tests
`$ composer test-apis` | Apis tests
`$ composer test-models` | Models tests
`$ composer test-mock` | Mock feature tests
`$ composer test-utils` | Utils tests
#### Config
@@ -110,6 +112,8 @@ Switch on option in `./index.php`:
+++ $app->addErrorMiddleware(true, true, true);
```
## [Mock Server Documentation]({{docsBasePath}}/MockServer.md)
{{#generateApiDocs}}
## API Endpoints

View File

@@ -41,6 +41,8 @@ use Dyorg\TokenAuthentication;
use Dyorg\TokenAuthentication\TokenSearch;
use Psr\Http\Message\ServerRequestInterface;
use {{invokerPackage}}\Middleware\JsonBodyParserMiddleware;
use {{mockPackage}}\OpenApiDataMocker;
use {{mockPackage}}\OpenApiDataMockerMiddleware;
use Exception;
/**
@@ -69,6 +71,15 @@ class SlimRouter
'classname' => '{{classname}}',
'userClassname' => '{{userClassname}}',
'operationId' => '{{operationId}}',
'responses' => [
{{#responses}}
'{{#isDefault}}default{{/isDefault}}{{^isDefault}}{{code}}{{/isDefault}}' => [
'code' => {{code}},
'message' => '{{message}}',
'jsonSchema' => '{{{jsonSchema}}}',
],
{{/responses}}
],
'authMethods' => [
{{#hasAuthMethods}}
{{#authMethods}}
@@ -161,12 +172,13 @@ class SlimRouter
};
{{/hasAuthMethods}}
$userOptions = null;
if ($settings instanceof ContainerInterface && $settings->has('tokenAuthenticationOptions')) {
$userOptions = $settings->get('tokenAuthenticationOptions');
} elseif (is_array($settings) && isset($settings['tokenAuthenticationOptions'])) {
$userOptions = $settings['tokenAuthenticationOptions'];
}
$userOptions = $this->getSetting($settings, 'tokenAuthenticationOptions', null);
// mocker options
$mockerOptions = $this->getSetting($settings, 'mockerOptions', null);
$dataMocker = $mockerOptions['dataMocker'] ?? new OpenApiDataMocker();
$getMockResponseCallback = $mockerOptions['getMockResponseCallback'] ?? null;
$mockAfterCallback = $mockerOptions['afterCallback'] ?? null;
foreach ($this->operations as $operation) {
$callback = function ($request, $response, $arguments) use ($operation) {
@@ -235,6 +247,10 @@ class SlimRouter
}
{{/hasAuthMethods}}
if (is_callable($getMockResponseCallback)) {
$middlewares[] = new OpenApiDataMockerMiddleware($dataMocker, $operation['responses'], $getMockResponseCallback, $mockAfterCallback);
}
$this->addRoute(
[$operation['httpMethod']],
"{$operation['basePathWithoutHost']}{$operation['path']}",
@@ -261,6 +277,26 @@ class SlimRouter
return array_merge($userOptions, $staticOptions);
}
/**
* Returns app setting by name.
*
* @param ContainerInterface|array $settings Either a ContainerInterface or an associative array of app settings
* @param string $settingName Setting name
* @param mixed $default Default setting value.
*
* @return mixed
*/
private function getSetting($settings, $settingName, $default = null)
{
if ($settings instanceof ContainerInterface && $settings->has($settingName)) {
return $settings->get($settingName);
} elseif (is_array($settings) && array_key_exists($settingName, $settings)) {
return $settings[$settingName];
}
return $default;
}
/**
* Add route with multiple methods
*

View File

@@ -14,6 +14,9 @@
require_once __DIR__ . '/vendor/autoload.php';
use {{invokerPackage}}\SlimRouter;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\ResponseInterface;
use {{mockPackage}}\OpenApiDataMocker;
{{/apiInfo}}
$config = [];
@@ -51,6 +54,35 @@ $config['tokenAuthenticationOptions'] = [
// 'error' => null,
];
/**
* Mocker Middleware options.
*/
$config['mockerOptions'] = [
// 'dataMocker' => new OpenApiDataMocker(),
// 'getMockResponseCallback' => function (ServerRequestInterface $request, array $responses) {
// // check if client clearly asks for mocked response
// if (
// $request->hasHeader('X-{{invokerPackage}}-Mock')
// && $request->getHeader('X-{{invokerPackage}}-Mock')[0] === 'ping'
// ) {
// if (array_key_exists('default', $responses)) {
// return $responses['default'];
// }
// // return first response
// return $responses[array_key_first($responses)];
// }
// return false;
// },
// 'afterCallback' => function ($request, $response) {
// // mark mocked response to distinguish real and fake responses
// return $response->withHeader('X-{{invokerPackage}}-Mock', 'pong');
// },
];
$router = new SlimRouter($config);
$app = $router->getSlimApp();

View File

@@ -0,0 +1,135 @@
# {{packageName}} - PHP Slim 4 Server library for {{appName}}
## Mock Server Documentation
### Mocker Options
To enable mock server uncomment these lines in `index.php` config file:
```php
/**
* Mocker Middleware options.
*/
$config['mockerOptions'] = [
'dataMocker' => new OpenApiDataMocker(),
'getMockResponseCallback' => function (ServerRequestInterface $request, array $responses) {
// check if client clearly asks for mocked response
if (
$request->hasHeader('X-{{invokerPackage}}-Mock')
&& $request->getHeader('X-{{invokerPackage}}-Mock')[0] === 'ping'
) {
if (array_key_exists('default', $responses)) {
return $responses['default'];
}
// return first response
return $responses[array_key_first($responses)];
}
return false;
},
'afterCallback' => function ($request, $response) {
// mark mocked response to distinguish real and fake responses
return $response->withHeader('X-{{invokerPackage}}-Mock', 'pong');
},
];
```
* `dataMocker` is mocker class instance. To create custom data mocker extend `{{mockPackage}}\{{interfaceNamePrefix}}OpenApiDataMocker{{interfaceNameSuffix}}`.
* `getMockResponseCallback` is callback before mock data generation. Above example shows how to enable mock feature for only requests with `{{X-{{invokerPackage}}}}-mock: ping` HTTP header. Adjust requests filtering to fit your project requirements. This function must return single response schema from `$responses` array parameter. **Mock feature is disabled when callback returns anything beside array.**
* `afterCallback` is callback executed after mock data generation. Most obvious use case is append specific HTTP headers to distinguish real and fake responses. **This function must always return response instance.**
### Supported features
All data types supported except specific string formats: `email`, `uuid`, `password` which are poorly implemented.
#### Data Types Support
| Data Type | Data Format | Supported |
|:---------:|:-----------:|:------------------:|
| `integer` | `int32` | :white_check_mark: |
| `integer` | `int64` | :white_check_mark: |
| `number` | `float` | :white_check_mark: |
| `number` | `double` | |
| `string` | `byte` | :white_check_mark: |
| `string` | `binary` | :white_check_mark: |
| `boolean` | | :white_check_mark: |
| `string` | `date` | :white_check_mark: |
| `string` | `date-time` | :white_check_mark: |
| `string` | `password` | :white_check_mark: |
| `string` | `email` | :white_check_mark: |
| `string` | `uuid` | :white_check_mark: |
#### Data Options Support
| Data Type | Option | Supported |
|:-----------:|:----------------------:|:------------------:|
| `string` | `minLength` | :white_check_mark: |
| `string` | `maxLength` | :white_check_mark: |
| `string` | `enum` | :white_check_mark: |
| `string` | `pattern` | |
| `integer` | `minimum` | :white_check_mark: |
| `integer` | `maximum` | :white_check_mark: |
| `integer` | `exclusiveMinimum` | :white_check_mark: |
| `integer` | `exclusiveMaximum` | :white_check_mark: |
| `number` | `minimum` | :white_check_mark: |
| `number` | `maximum` | :white_check_mark: |
| `number` | `exclusiveMinimum` | :white_check_mark: |
| `number` | `exclusiveMaximum` | :white_check_mark: |
| `array` | `items` | :white_check_mark: |
| `array` | `additionalItems` | |
| `array` | `minItems` | :white_check_mark: |
| `array` | `maxItems` | :white_check_mark: |
| `array` | `uniqueItems` | |
| `object` | `properties` | :white_check_mark: |
| `object` | `maxProperties` | |
| `object` | `minProperties` | |
| `object` | `patternProperties` | |
| `object` | `additionalProperties` | |
| `object` | `required` | |
| `*` | `$ref` | :white_check_mark: |
| `*` | `allOf` | |
| `*` | `anyOf` | |
| `*` | `oneOf` | |
| `*` | `not` | |
### Known Limitations
Avoid circular refs in your schema. Schema below can cause infinite loop and `Out of Memory` PHP error:
```yml
# ModelA has reference to ModelB while ModelB has reference to ModelA.
# Mock server will produce huge nested JSON example and ended with `Out of Memory` error.
definitions:
ModelA:
type: object
properties:
model_b:
$ref: '#/definitions/ModelB'
ModelB:
type: array
items:
$ref: '#/definitions/ModelA'
```
Don't ref scalar types, because generator will not produce models which mock server can find. So schema below will cause error:
```yml
# generated build contains only `OuterComposite` model class which referenced to not existed `OuterNumber`, `OuterString`, `OuterBoolean` classes
# mock server cannot mock `OuterComposite` model and throws exception
definitions:
OuterComposite:
type: object
properties:
my_number:
$ref: '#/definitions/OuterNumber'
my_string:
$ref: '#/definitions/OuterString'
my_boolean:
$ref: '#/definitions/OuterBoolean'
OuterNumber:
type: number
OuterString:
type: string
OuterBoolean:
type: boolean
```

View File

@@ -0,0 +1,195 @@
<?php
/**
* OpenApiDataMockerMiddleware
*
* PHP version 7.1
*
* @package {{invokerPackage}}
* @author OpenAPI Generator team
* @link https://github.com/openapitools/openapi-generator
*/
/**{{#apiInfo}}{{#appName}}
* {{{appName}}}
*
{{/appName}}
{{#appDescription}}
* {{{appDescription}}}
{{/appDescription}}
{{#version}}
* The version of the OpenAPI document: {{{version}}}
{{/version}}
{{#infoEmail}}
* Contact: {{{infoEmail}}}
{{/infoEmail}}
* Generated by: https://github.com/openapitools/openapi-generator.git
*/
/**
* NOTE: This class is auto generated by the openapi generator program.
* https://github.com/openapitools/openapi-generator
* Do not edit the class manually.
*/
namespace {{mockPackage}};
use Slim\Factory\AppFactory;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use {{mockPackage}}\{{interfaceNamePrefix}}OpenApiDataMocker{{interfaceNameSuffix}};
use InvalidArgumentException;
/**
* OpenApiDataMockerMiddleware Class Doc Comment
*
* @package {{mockPackage}}
* @author OpenAPI Generator team
* @link https://github.com/openapitools/openapi-generator
*/
final class OpenApiDataMockerMiddleware implements MiddlewareInterface
{
/**
* @var {{interfaceNamePrefix}}OpenApiDataMocker{{interfaceNameSuffix}} DataMocker.
*/
private $mocker;
/**
* @var array Array of responses schemas.
*/
private $responses;
/**
* @var callable|null Custom callback to select mocked response.
*/
private $getMockResponseCallback;
/**
* @var callable|null Custom after callback.
*/
private $afterCallback;
/**
* Class constructor.
*
* @param {{interfaceNamePrefix}}OpenApiDataMocker{{interfaceNameSuffix}} $mocker DataMocker.
* @param array $responses Array of responses schemas.
* @param callable|null $getMockResponseCallback Custom callback to select mocked response.
* Mock feature is disabled when this argument is null.
* @example $getMockResponseCallback = function (ServerRequestInterface $request, array $responses) {
* // check if client clearly asks for mocked response
* if (
* $request->hasHeader('X-{{invokerPackage}}-Mock')
* && $request->header('X-{{invokerPackage}}-Mock')[0] === 'ping'
* ) {
* return $responses[array_key_first($responses)];
* }
* return false;
* };
* @param callable|null $afterCallback After callback.
* Function must return response instance.
* @example $afterCallback = function (ServerRequestInterface $request, ResponseInterface $response) {
* // mark mocked response to distinguish real and fake responses
* return $response->withHeader('X-{{invokerPackage}}-Mock', 'pong');
* };
*/
public function __construct(
{{interfaceNamePrefix}}OpenApiDataMocker{{interfaceNameSuffix}} $mocker,
array $responses,
$getMockResponseCallback = null,
$afterCallback = null
) {
$this->mocker = $mocker;
$this->responses = $responses;
if (is_callable($getMockResponseCallback)) {
$this->getMockResponseCallback = $getMockResponseCallback;
} elseif ($getMockResponseCallback !== null) {
// wrong argument type
throw new InvalidArgumentException('\$getMockResponseCallback must be closure or null');
}
if (is_callable($afterCallback)) {
$this->afterCallback = $afterCallback;
} elseif ($afterCallback !== null) {
// wrong argument type
throw new InvalidArgumentException('\$afterCallback must be closure or null');
}
}
/**
* Parse incoming JSON input into a native PHP format
*
* @param ServerRequestInterface $request HTTP request
* @param RequestHandlerInterface $handler Request handler
*
* @return ResponseInterface HTTP response
*/
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$customCallback = $this->getMockResponseCallback;
$customAfterCallback = $this->afterCallback;
$mockedResponse = (is_callable($customCallback)) ? $customCallback($request, $this->responses) : null;
if (
is_array($mockedResponse)
&& array_key_exists('code', $mockedResponse)
&& array_key_exists('jsonSchema', $mockedResponse)
) {
// response schema succesfully selected, we can mock it now
$statusCode = ($mockedResponse['code'] === 0) ? 200 : $mockedResponse['code'];
$contentType = '*/*';
$response = AppFactory::determineResponseFactory()->createResponse($statusCode);
$responseSchema = json_decode($mockedResponse['jsonSchema'], true);
if (is_array($responseSchema) && array_key_exists('headers', $responseSchema)) {
// response schema contains headers definitions, apply them one by one
foreach ($responseSchema['headers'] as $headerName => $headerDefinition) {
$response = $response->withHeader($headerName, $this->mocker->mockFromSchema($headerDefinition['schema']));
}
}
if (
is_array($responseSchema)
&& array_key_exists('content', $responseSchema)
&& !empty($responseSchema['content'])
) {
// response schema contains body definition
$responseContentSchema = null;
foreach ($responseSchema['content'] as $schemaContentType => $schemaDefinition) {
// we can respond in JSON format when any(*/*) content-type allowed
// or JSON(application/json) content-type specifically defined
if (
$schemaContentType === '*/*'
|| strtolower(substr($schemaContentType, 0, 16)) === 'application/json'
) {
$contentType = 'application/json';
$responseContentSchema = $schemaDefinition['schema'];
}
}
if ($contentType === 'application/json') {
$responseBody = $this->mocker->mockFromSchema($responseContentSchema);
$response->getBody()->write(json_encode($responseBody));
} else {
// notify developer that only application/json response supported so far
$response->getBody()->write('Mock feature supports only "application/json" content-type!');
}
}
// after callback applied only when mocked response schema has been selected
if (is_callable($customAfterCallback)) {
$response = $customAfterCallback($request, $response);
}
// no reason to execute following middlewares (auth, validation etc.)
// return mocked response and end connection
return $response
->withHeader('Content-Type', $contentType);
}
// no response selected, mock feature disabled
// execute following middlewares
return $handler->handle($request);
}
}
{{/apiInfo}}

View File

@@ -0,0 +1,282 @@
<?php
/**
* OpenApiDataMockerMiddlewareTest
*
* PHP version 7.1
*
* @package {{invokerPackage}}
* @author OpenAPI Generator team
* @link https://github.com/openapitools/openapi-generator
*/
/**{{#apiInfo}}{{#invokerPackage}}
* {{{invokerPackage}}}
*
{{/invokerPackage}}
{{#appDescription}}
* {{{appDescription}}}
{{/appDescription}}
{{#version}}
* The version of the OpenAPI document: {{{version}}}
{{/version}}
{{#infoEmail}}
* Contact: {{{infoEmail}}}
{{/infoEmail}}
* Generated by: https://github.com/openapitools/openapi-generator.git
*/
/**
* NOTE: This class is auto generated by the openapi generator program.
* https://github.com/openapitools/openapi-generator
* Do not edit the class manually.
*/
namespace {{mockPackage}};
use {{mockPackage}}\OpenApiDataMockerMiddleware;
use {{mockPackage}}\OpenApiDataMocker;
use Slim\Factory\AppFactory;
use Slim\Factory\ServerRequestCreatorFactory;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use PHPUnit\Framework\TestCase;
use StdClass;
/**
* OpenApiDataMockerMiddlewareTest Class Doc Comment
*
* @package {{mockPackage}}
* @author OpenAPI Generator team
* @link https://github.com/openapitools/openapi-generator
* @coversDefaultClass \{{mockPackage}}\OpenApiDataMockerMiddleware
*/
class OpenApiDataMockerMiddlewareTest extends TestCase
{
/**
* @covers ::__construct
* @dataProvider provideConstructCorrectArguments
*/
public function testConstructor(
$mocker,
$responses,
$getMockResponseCallback,
$afterCallback
) {
$middleware = new OpenApiDataMockerMiddleware($mocker, $responses, $getMockResponseCallback, $afterCallback);
$this->assertInstanceOf(OpenApiDataMockerMiddleware::class, $middleware);
$this->assertNotNull($middleware);
}
public function provideConstructCorrectArguments()
{
$getMockResponseCallback = function () {
return false;
};
$afterCallback = function () {
return false;
};
return [
[new OpenApiDataMocker(), [], null, null],
[new OpenApiDataMocker(), [], $getMockResponseCallback, $afterCallback],
];
}
/**
* @covers ::__construct
* @dataProvider provideConstructInvalidArguments
* @expectedException \InvalidArgumentException
* @expectedException \TypeError
*/
public function testConstructorWithInvalidArguments(
$mocker,
$responses,
$getMockResponseCallback,
$afterCallback
) {
$middleware = new OpenApiDataMockerMiddleware($mocker, $responses, $getMockResponseCallback, $afterCallback);
}
public function provideConstructInvalidArguments()
{
return [
'getMockResponseCallback not callable' => [
new OpenApiDataMocker(), [], 'foobar', null,
],
'afterCallback not callable' => [
new OpenApiDataMocker(), [], null, 'foobar',
],
];
}
/**
* @covers ::process
* @dataProvider provideProcessArguments
*/
public function testProcess(
$mocker,
$responses,
$getMockResponseCallback,
$afterCallback,
$request,
$expectedStatusCode,
$expectedHeaders,
$notExpectedHeaders,
$expectedBody
) {
// Create a stub for the RequestHandlerInterface interface.
$handler = $this->createMock(RequestHandlerInterface::class);
$handler->method('handle')
->willReturn(AppFactory::determineResponseFactory()->createResponse());
$middleware = new OpenApiDataMockerMiddleware(
$mocker,
$responses,
$getMockResponseCallback,
$afterCallback
);
$response = $middleware->process($request, $handler);
// check status code
$this->assertSame($expectedStatusCode, $response->getStatusCode());
// check http headers in request
foreach ($expectedHeaders as $expectedHeader => $expectedHeaderValue) {
$this->assertTrue($response->hasHeader($expectedHeader));
if ($expectedHeaderValue !== '*') {
$this->assertSame($expectedHeaderValue, $response->getHeader($expectedHeader)[0]);
}
}
foreach ($notExpectedHeaders as $notExpectedHeader) {
$this->assertFalse($response->hasHeader($notExpectedHeader));
}
// check body
if (is_array($expectedBody)) {
// random values, check keys only
foreach ($expectedBody as $attribute => $value) {
$this->assertObjectHasAttribute($attribute, json_decode((string) $response->getBody(), false));
}
} else {
$this->assertEquals($expectedBody, (string) $response->getBody());
}
}
public function provideProcessArguments()
{
$mocker = new OpenApiDataMocker();
$isMockResponseRequired = function (ServerRequestInterface $request) {
$mockHttpHeader = 'X-{{invokerPackage}}-Mock';
return $request->hasHeader($mockHttpHeader)
&& $request->getHeader($mockHttpHeader)[0] === 'ping';
};
$getMockResponseCallback = function (ServerRequestInterface $request, array $responses) use ($isMockResponseRequired) {
if ($isMockResponseRequired($request)) {
if (array_key_exists('default', $responses)) {
return $responses['default'];
}
// return first response
return $responses[array_key_first($responses)];
}
return false;
};
$afterCallback = function ($request, $response) use ($isMockResponseRequired) {
if ($isMockResponseRequired($request)) {
$response = $response->withHeader('X-{{invokerPackage}}-Mock', 'pong');
}
return $response;
};
$responses = [
'400' => [
'code' => 400,
'jsonSchema' => json_encode([
'description' => 'Bad Request Response',
'content' => new StdClass(),
]),
],
'default' => [
'code' => 201,
'jsonSchema' => json_encode([
'description' => 'Success Response',
'headers' => [
'X-Location' => ['schema' => ['type' => 'string']],
'X-Created-Id' => ['schema' => ['type' => 'integer']],
],
'content' => [
'application/json;encoding=utf-8' => ['schema' => ['type' => 'object', 'properties' => ['id' => ['type' => 'integer'], 'className' => ['type' => 'string'], 'declawed' => ['type' => 'boolean']]]],
],
]),
],
];
$responsesXmlOnly = [
'default' => [
'code' => 201,
'jsonSchema' => json_encode([
'description' => 'Success Response',
'content' => [
'application/xml' => [
'schema' => [
'type' => 'string',
],
],
],
]),
],
];
$requestFactory = ServerRequestCreatorFactory::create();
return [
'callbacks null' => [
$mocker,
$responses,
null,
null,
$requestFactory->createServerRequestFromGlobals(),
200,
[],
['X-{{invokerPackage}}-Mock', 'x-location', 'x-created-id'],
'',
],
'xml not supported' => [
$mocker,
$responsesXmlOnly,
$getMockResponseCallback,
$afterCallback,
$requestFactory
->createServerRequestFromGlobals()
->withHeader('X-{{invokerPackage}}-Mock', 'ping'),
201,
['X-{{invokerPackage}}-Mock' => 'pong', 'content-type' => '*/*'],
['x-location', 'x-created-id'],
'Mock feature supports only "application/json" content-type!',
],
'mock response default schema' => [
$mocker,
$responses,
$getMockResponseCallback,
$afterCallback,
$requestFactory
->createServerRequestFromGlobals()
->withHeader('X-{{invokerPackage}}-Mock', 'ping'),
201,
['X-{{invokerPackage}}-Mock' => 'pong', 'content-type' => 'application/json', 'x-location' => '*', 'x-created-id' => '*'],
[],
[
'id' => 1,
'className' => 'cat',
'declawed' => false,
],
],
];
}
}
{{/apiInfo}}

View File

@@ -46,6 +46,8 @@ Command | Target
`$ composer test` | All tests
`$ composer test-apis` | Apis tests
`$ composer test-models` | Models tests
`$ composer test-mock` | Mock feature tests
`$ composer test-utils` | Utils tests
#### Config
@@ -99,6 +101,8 @@ Switch on option in `./index.php`:
+++ $app->addErrorMiddleware(true, true, true);
```
## [Mock Server Documentation](./docs/MockServer.md)
## API Endpoints
All URIs are relative to *http://petstore.swagger.io:80/v2*

View File

@@ -0,0 +1,135 @@
# php-base - PHP Slim 4 Server library for OpenAPI Petstore
## Mock Server Documentation
### Mocker Options
To enable mock server uncomment these lines in `index.php` config file:
```php
/**
* Mocker Middleware options.
*/
$config['mockerOptions'] = [
'dataMocker' => new OpenApiDataMocker(),
'getMockResponseCallback' => function (ServerRequestInterface $request, array $responses) {
// check if client clearly asks for mocked response
if (
$request->hasHeader('X-OpenAPIServer-Mock')
&& $request->getHeader('X-OpenAPIServer-Mock')[0] === 'ping'
) {
if (array_key_exists('default', $responses)) {
return $responses['default'];
}
// return first response
return $responses[array_key_first($responses)];
}
return false;
},
'afterCallback' => function ($request, $response) {
// mark mocked response to distinguish real and fake responses
return $response->withHeader('X-OpenAPIServer-Mock', 'pong');
},
];
```
* `dataMocker` is mocker class instance. To create custom data mocker extend `OpenAPIServer\Mock\OpenApiDataMockerInterface`.
* `getMockResponseCallback` is callback before mock data generation. Above example shows how to enable mock feature for only requests with `{{X-OpenAPIServer}}-mock: ping` HTTP header. Adjust requests filtering to fit your project requirements. This function must return single response schema from `$responses` array parameter. **Mock feature is disabled when callback returns anything beside array.**
* `afterCallback` is callback executed after mock data generation. Most obvious use case is append specific HTTP headers to distinguish real and fake responses. **This function must always return response instance.**
### Supported features
All data types supported except specific string formats: `email`, `uuid`, `password` which are poorly implemented.
#### Data Types Support
| Data Type | Data Format | Supported |
|:---------:|:-----------:|:------------------:|
| `integer` | `int32` | :white_check_mark: |
| `integer` | `int64` | :white_check_mark: |
| `number` | `float` | :white_check_mark: |
| `number` | `double` | |
| `string` | `byte` | :white_check_mark: |
| `string` | `binary` | :white_check_mark: |
| `boolean` | | :white_check_mark: |
| `string` | `date` | :white_check_mark: |
| `string` | `date-time` | :white_check_mark: |
| `string` | `password` | :white_check_mark: |
| `string` | `email` | :white_check_mark: |
| `string` | `uuid` | :white_check_mark: |
#### Data Options Support
| Data Type | Option | Supported |
|:-----------:|:----------------------:|:------------------:|
| `string` | `minLength` | :white_check_mark: |
| `string` | `maxLength` | :white_check_mark: |
| `string` | `enum` | :white_check_mark: |
| `string` | `pattern` | |
| `integer` | `minimum` | :white_check_mark: |
| `integer` | `maximum` | :white_check_mark: |
| `integer` | `exclusiveMinimum` | :white_check_mark: |
| `integer` | `exclusiveMaximum` | :white_check_mark: |
| `number` | `minimum` | :white_check_mark: |
| `number` | `maximum` | :white_check_mark: |
| `number` | `exclusiveMinimum` | :white_check_mark: |
| `number` | `exclusiveMaximum` | :white_check_mark: |
| `array` | `items` | :white_check_mark: |
| `array` | `additionalItems` | |
| `array` | `minItems` | :white_check_mark: |
| `array` | `maxItems` | :white_check_mark: |
| `array` | `uniqueItems` | |
| `object` | `properties` | :white_check_mark: |
| `object` | `maxProperties` | |
| `object` | `minProperties` | |
| `object` | `patternProperties` | |
| `object` | `additionalProperties` | |
| `object` | `required` | |
| `*` | `$ref` | :white_check_mark: |
| `*` | `allOf` | |
| `*` | `anyOf` | |
| `*` | `oneOf` | |
| `*` | `not` | |
### Known Limitations
Avoid circular refs in your schema. Schema below can cause infinite loop and `Out of Memory` PHP error:
```yml
# ModelA has reference to ModelB while ModelB has reference to ModelA.
# Mock server will produce huge nested JSON example and ended with `Out of Memory` error.
definitions:
ModelA:
type: object
properties:
model_b:
$ref: '#/definitions/ModelB'
ModelB:
type: array
items:
$ref: '#/definitions/ModelA'
```
Don't ref scalar types, because generator will not produce models which mock server can find. So schema below will cause error:
```yml
# generated build contains only `OuterComposite` model class which referenced to not existed `OuterNumber`, `OuterString`, `OuterBoolean` classes
# mock server cannot mock `OuterComposite` model and throws exception
definitions:
OuterComposite:
type: object
properties:
my_number:
$ref: '#/definitions/OuterNumber'
my_string:
$ref: '#/definitions/OuterString'
my_boolean:
$ref: '#/definitions/OuterBoolean'
OuterNumber:
type: number
OuterString:
type: string
OuterBoolean:
type: boolean
```

View File

@@ -14,6 +14,9 @@
require_once __DIR__ . '/vendor/autoload.php';
use OpenAPIServer\SlimRouter;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\ResponseInterface;
use OpenAPIServer\Mock\OpenApiDataMocker;
$config = [];
@@ -50,6 +53,35 @@ $config['tokenAuthenticationOptions'] = [
// 'error' => null,
];
/**
* Mocker Middleware options.
*/
$config['mockerOptions'] = [
// 'dataMocker' => new OpenApiDataMocker(),
// 'getMockResponseCallback' => function (ServerRequestInterface $request, array $responses) {
// // check if client clearly asks for mocked response
// if (
// $request->hasHeader('X-OpenAPIServer-Mock')
// && $request->getHeader('X-OpenAPIServer-Mock')[0] === 'ping'
// ) {
// if (array_key_exists('default', $responses)) {
// return $responses['default'];
// }
// // return first response
// return $responses[array_key_first($responses)];
// }
// return false;
// },
// 'afterCallback' => function ($request, $response) {
// // mark mocked response to distinguish real and fake responses
// return $response->withHeader('X-OpenAPIServer-Mock', 'pong');
// },
];
$router = new SlimRouter($config);
$app = $router->getSlimApp();

View File

@@ -0,0 +1,186 @@
<?php
/**
* OpenApiDataMockerMiddleware
*
* PHP version 7.1
*
* @package OpenAPIServer
* @author OpenAPI Generator team
* @link https://github.com/openapitools/openapi-generator
*/
/**
* OpenAPI Petstore
*
* This spec is mainly for testing Petstore server and contains fake endpoints, models. Please do not use this for any other purpose. Special characters: \" \\
* The version of the OpenAPI document: 1.0.0
* Generated by: https://github.com/openapitools/openapi-generator.git
*/
/**
* NOTE: This class is auto generated by the openapi generator program.
* https://github.com/openapitools/openapi-generator
* Do not edit the class manually.
*/
namespace OpenAPIServer\Mock;
use Slim\Factory\AppFactory;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use OpenAPIServer\Mock\OpenApiDataMockerInterface;
use InvalidArgumentException;
/**
* OpenApiDataMockerMiddleware Class Doc Comment
*
* @package OpenAPIServer\Mock
* @author OpenAPI Generator team
* @link https://github.com/openapitools/openapi-generator
*/
final class OpenApiDataMockerMiddleware implements MiddlewareInterface
{
/**
* @var OpenApiDataMockerInterface DataMocker.
*/
private $mocker;
/**
* @var array Array of responses schemas.
*/
private $responses;
/**
* @var callable|null Custom callback to select mocked response.
*/
private $getMockResponseCallback;
/**
* @var callable|null Custom after callback.
*/
private $afterCallback;
/**
* Class constructor.
*
* @param OpenApiDataMockerInterface $mocker DataMocker.
* @param array $responses Array of responses schemas.
* @param callable|null $getMockResponseCallback Custom callback to select mocked response.
* Mock feature is disabled when this argument is null.
* @example $getMockResponseCallback = function (ServerRequestInterface $request, array $responses) {
* // check if client clearly asks for mocked response
* if (
* $request->hasHeader('X-OpenAPIServer-Mock')
* && $request->header('X-OpenAPIServer-Mock')[0] === 'ping'
* ) {
* return $responses[array_key_first($responses)];
* }
* return false;
* };
* @param callable|null $afterCallback After callback.
* Function must return response instance.
* @example $afterCallback = function (ServerRequestInterface $request, ResponseInterface $response) {
* // mark mocked response to distinguish real and fake responses
* return $response->withHeader('X-OpenAPIServer-Mock', 'pong');
* };
*/
public function __construct(
OpenApiDataMockerInterface $mocker,
array $responses,
$getMockResponseCallback = null,
$afterCallback = null
) {
$this->mocker = $mocker;
$this->responses = $responses;
if (is_callable($getMockResponseCallback)) {
$this->getMockResponseCallback = $getMockResponseCallback;
} elseif ($getMockResponseCallback !== null) {
// wrong argument type
throw new InvalidArgumentException('\$getMockResponseCallback must be closure or null');
}
if (is_callable($afterCallback)) {
$this->afterCallback = $afterCallback;
} elseif ($afterCallback !== null) {
// wrong argument type
throw new InvalidArgumentException('\$afterCallback must be closure or null');
}
}
/**
* Parse incoming JSON input into a native PHP format
*
* @param ServerRequestInterface $request HTTP request
* @param RequestHandlerInterface $handler Request handler
*
* @return ResponseInterface HTTP response
*/
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$customCallback = $this->getMockResponseCallback;
$customAfterCallback = $this->afterCallback;
$mockedResponse = (is_callable($customCallback)) ? $customCallback($request, $this->responses) : null;
if (
is_array($mockedResponse)
&& array_key_exists('code', $mockedResponse)
&& array_key_exists('jsonSchema', $mockedResponse)
) {
// response schema succesfully selected, we can mock it now
$statusCode = ($mockedResponse['code'] === 0) ? 200 : $mockedResponse['code'];
$contentType = '*/*';
$response = AppFactory::determineResponseFactory()->createResponse($statusCode);
$responseSchema = json_decode($mockedResponse['jsonSchema'], true);
if (is_array($responseSchema) && array_key_exists('headers', $responseSchema)) {
// response schema contains headers definitions, apply them one by one
foreach ($responseSchema['headers'] as $headerName => $headerDefinition) {
$response = $response->withHeader($headerName, $this->mocker->mockFromSchema($headerDefinition['schema']));
}
}
if (
is_array($responseSchema)
&& array_key_exists('content', $responseSchema)
&& !empty($responseSchema['content'])
) {
// response schema contains body definition
$responseContentSchema = null;
foreach ($responseSchema['content'] as $schemaContentType => $schemaDefinition) {
// we can respond in JSON format when any(*/*) content-type allowed
// or JSON(application/json) content-type specifically defined
if (
$schemaContentType === '*/*'
|| strtolower(substr($schemaContentType, 0, 16)) === 'application/json'
) {
$contentType = 'application/json';
$responseContentSchema = $schemaDefinition['schema'];
}
}
if ($contentType === 'application/json') {
$responseBody = $this->mocker->mockFromSchema($responseContentSchema);
$response->getBody()->write(json_encode($responseBody));
} else {
// notify developer that only application/json response supported so far
$response->getBody()->write('Mock feature supports only "application/json" content-type!');
}
}
// after callback applied only when mocked response schema has been selected
if (is_callable($customAfterCallback)) {
$response = $customAfterCallback($request, $response);
}
// no reason to execute following middlewares (auth, validation etc.)
// return mocked response and end connection
return $response
->withHeader('Content-Type', $contentType);
}
// no response selected, mock feature disabled
// execute following middlewares
return $handler->handle($request);
}
}

View File

@@ -33,6 +33,8 @@ use Dyorg\TokenAuthentication;
use Dyorg\TokenAuthentication\TokenSearch;
use Psr\Http\Message\ServerRequestInterface;
use OpenAPIServer\Middleware\JsonBodyParserMiddleware;
use OpenAPIServer\Mock\OpenApiDataMocker;
use OpenAPIServer\Mock\OpenApiDataMockerMiddleware;
use Exception;
/**
@@ -58,6 +60,22 @@ class SlimRouter
'classname' => 'AbstractAnotherFakeApi',
'userClassname' => 'AnotherFakeApi',
'operationId' => 'call123TestSpecialTags',
'responses' => [
'default' => [
'code' => 200,
'message' => 'successful operation',
'jsonSchema' => '{
"description" : "successful operation",
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/Client"
}
}
}
}',
],
],
'authMethods' => [
],
],
@@ -69,6 +87,16 @@ class SlimRouter
'classname' => 'AbstractFakeApi',
'userClassname' => 'FakeApi',
'operationId' => 'createXmlItem',
'responses' => [
'default' => [
'code' => 200,
'message' => 'successful operation',
'jsonSchema' => '{
"description" : "successful operation",
"content" : { }
}',
],
],
'authMethods' => [
],
],
@@ -80,6 +108,22 @@ class SlimRouter
'classname' => 'AbstractFakeApi',
'userClassname' => 'FakeApi',
'operationId' => 'fakeOuterBooleanSerialize',
'responses' => [
'default' => [
'code' => 200,
'message' => 'Output boolean',
'jsonSchema' => '{
"description" : "Output boolean",
"content" : {
"*/*" : {
"schema" : {
"$ref" : "#/components/schemas/OuterBoolean"
}
}
}
}',
],
],
'authMethods' => [
],
],
@@ -91,6 +135,22 @@ class SlimRouter
'classname' => 'AbstractFakeApi',
'userClassname' => 'FakeApi',
'operationId' => 'fakeOuterCompositeSerialize',
'responses' => [
'default' => [
'code' => 200,
'message' => 'Output composite',
'jsonSchema' => '{
"description" : "Output composite",
"content" : {
"*/*" : {
"schema" : {
"$ref" : "#/components/schemas/OuterComposite"
}
}
}
}',
],
],
'authMethods' => [
],
],
@@ -102,6 +162,22 @@ class SlimRouter
'classname' => 'AbstractFakeApi',
'userClassname' => 'FakeApi',
'operationId' => 'fakeOuterNumberSerialize',
'responses' => [
'default' => [
'code' => 200,
'message' => 'Output number',
'jsonSchema' => '{
"description" : "Output number",
"content" : {
"*/*" : {
"schema" : {
"$ref" : "#/components/schemas/OuterNumber"
}
}
}
}',
],
],
'authMethods' => [
],
],
@@ -113,6 +189,22 @@ class SlimRouter
'classname' => 'AbstractFakeApi',
'userClassname' => 'FakeApi',
'operationId' => 'fakeOuterStringSerialize',
'responses' => [
'default' => [
'code' => 200,
'message' => 'Output string',
'jsonSchema' => '{
"description" : "Output string",
"content" : {
"*/*" : {
"schema" : {
"$ref" : "#/components/schemas/OuterString"
}
}
}
}',
],
],
'authMethods' => [
],
],
@@ -124,6 +216,16 @@ class SlimRouter
'classname' => 'AbstractFakeApi',
'userClassname' => 'FakeApi',
'operationId' => 'testBodyWithFileSchema',
'responses' => [
'default' => [
'code' => 200,
'message' => 'Success',
'jsonSchema' => '{
"description" : "Success",
"content" : { }
}',
],
],
'authMethods' => [
],
],
@@ -135,6 +237,16 @@ class SlimRouter
'classname' => 'AbstractFakeApi',
'userClassname' => 'FakeApi',
'operationId' => 'testBodyWithQueryParams',
'responses' => [
'default' => [
'code' => 200,
'message' => 'Success',
'jsonSchema' => '{
"description" : "Success",
"content" : { }
}',
],
],
'authMethods' => [
],
],
@@ -146,6 +258,22 @@ class SlimRouter
'classname' => 'AbstractFakeApi',
'userClassname' => 'FakeApi',
'operationId' => 'testClientModel',
'responses' => [
'default' => [
'code' => 200,
'message' => 'successful operation',
'jsonSchema' => '{
"description" : "successful operation",
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/Client"
}
}
}
}',
],
],
'authMethods' => [
],
],
@@ -157,6 +285,24 @@ class SlimRouter
'classname' => 'AbstractFakeApi',
'userClassname' => 'FakeApi',
'operationId' => 'testEndpointParameters',
'responses' => [
'400' => [
'code' => 400,
'message' => 'Invalid username supplied',
'jsonSchema' => '{
"description" : "Invalid username supplied",
"content" : { }
}',
],
'404' => [
'code' => 404,
'message' => 'User not found',
'jsonSchema' => '{
"description" : "User not found",
"content" : { }
}',
],
],
'authMethods' => [
// http security schema named 'http_basic_test'
[
@@ -176,6 +322,24 @@ class SlimRouter
'classname' => 'AbstractFakeApi',
'userClassname' => 'FakeApi',
'operationId' => 'testEnumParameters',
'responses' => [
'400' => [
'code' => 400,
'message' => 'Invalid request',
'jsonSchema' => '{
"description" : "Invalid request",
"content" : { }
}',
],
'404' => [
'code' => 404,
'message' => 'Not found',
'jsonSchema' => '{
"description" : "Not found",
"content" : { }
}',
],
],
'authMethods' => [
],
],
@@ -187,6 +351,16 @@ class SlimRouter
'classname' => 'AbstractFakeApi',
'userClassname' => 'FakeApi',
'operationId' => 'testGroupParameters',
'responses' => [
'400' => [
'code' => 400,
'message' => 'Someting wrong',
'jsonSchema' => '{
"description" : "Someting wrong",
"content" : { }
}',
],
],
'authMethods' => [
],
],
@@ -198,6 +372,16 @@ class SlimRouter
'classname' => 'AbstractFakeApi',
'userClassname' => 'FakeApi',
'operationId' => 'testInlineAdditionalProperties',
'responses' => [
'default' => [
'code' => 200,
'message' => 'successful operation',
'jsonSchema' => '{
"description" : "successful operation",
"content" : { }
}',
],
],
'authMethods' => [
],
],
@@ -209,6 +393,16 @@ class SlimRouter
'classname' => 'AbstractFakeApi',
'userClassname' => 'FakeApi',
'operationId' => 'testJsonFormData',
'responses' => [
'default' => [
'code' => 200,
'message' => 'successful operation',
'jsonSchema' => '{
"description" : "successful operation",
"content" : { }
}',
],
],
'authMethods' => [
],
],
@@ -220,6 +414,16 @@ class SlimRouter
'classname' => 'AbstractFakeApi',
'userClassname' => 'FakeApi',
'operationId' => 'testQueryParameterCollectionFormat',
'responses' => [
'default' => [
'code' => 200,
'message' => 'Success',
'jsonSchema' => '{
"description" : "Success",
"content" : { }
}',
],
],
'authMethods' => [
],
],
@@ -231,6 +435,22 @@ class SlimRouter
'classname' => 'AbstractFakeClassnameTags123Api',
'userClassname' => 'FakeClassnameTags123Api',
'operationId' => 'testClassname',
'responses' => [
'default' => [
'code' => 200,
'message' => 'successful operation',
'jsonSchema' => '{
"description" : "successful operation",
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/Client"
}
}
}
}',
],
],
'authMethods' => [
// apiKey security schema named 'api_key_query'
[
@@ -254,6 +474,24 @@ class SlimRouter
'classname' => 'AbstractPetApi',
'userClassname' => 'PetApi',
'operationId' => 'addPet',
'responses' => [
'default' => [
'code' => 200,
'message' => 'successful operation',
'jsonSchema' => '{
"description" : "successful operation",
"content" : { }
}',
],
'405' => [
'code' => 405,
'message' => 'Invalid input',
'jsonSchema' => '{
"description" : "Invalid input",
"content" : { }
}',
],
],
'authMethods' => [
// oauth2 security schema named 'petstore_auth'
[
@@ -277,6 +515,41 @@ class SlimRouter
'classname' => 'AbstractPetApi',
'userClassname' => 'PetApi',
'operationId' => 'findPetsByStatus',
'responses' => [
'default' => [
'code' => 200,
'message' => 'successful operation',
'jsonSchema' => '{
"description" : "successful operation",
"content" : {
"application/xml" : {
"schema" : {
"type" : "array",
"items" : {
"$ref" : "#/components/schemas/Pet"
}
}
},
"application/json" : {
"schema" : {
"type" : "array",
"items" : {
"$ref" : "#/components/schemas/Pet"
}
}
}
}
}',
],
'400' => [
'code' => 400,
'message' => 'Invalid status value',
'jsonSchema' => '{
"description" : "Invalid status value",
"content" : { }
}',
],
],
'authMethods' => [
// oauth2 security schema named 'petstore_auth'
[
@@ -300,6 +573,41 @@ class SlimRouter
'classname' => 'AbstractPetApi',
'userClassname' => 'PetApi',
'operationId' => 'findPetsByTags',
'responses' => [
'default' => [
'code' => 200,
'message' => 'successful operation',
'jsonSchema' => '{
"description" : "successful operation",
"content" : {
"application/xml" : {
"schema" : {
"type" : "array",
"items" : {
"$ref" : "#/components/schemas/Pet"
}
}
},
"application/json" : {
"schema" : {
"type" : "array",
"items" : {
"$ref" : "#/components/schemas/Pet"
}
}
}
}
}',
],
'400' => [
'code' => 400,
'message' => 'Invalid tag value',
'jsonSchema' => '{
"description" : "Invalid tag value",
"content" : { }
}',
],
],
'authMethods' => [
// oauth2 security schema named 'petstore_auth'
[
@@ -323,6 +631,40 @@ class SlimRouter
'classname' => 'AbstractPetApi',
'userClassname' => 'PetApi',
'operationId' => 'updatePet',
'responses' => [
'default' => [
'code' => 200,
'message' => 'successful operation',
'jsonSchema' => '{
"description" : "successful operation",
"content" : { }
}',
],
'400' => [
'code' => 400,
'message' => 'Invalid ID supplied',
'jsonSchema' => '{
"description" : "Invalid ID supplied",
"content" : { }
}',
],
'404' => [
'code' => 404,
'message' => 'Pet not found',
'jsonSchema' => '{
"description" : "Pet not found",
"content" : { }
}',
],
'405' => [
'code' => 405,
'message' => 'Validation exception',
'jsonSchema' => '{
"description" : "Validation exception",
"content" : { }
}',
],
],
'authMethods' => [
// oauth2 security schema named 'petstore_auth'
[
@@ -346,6 +688,24 @@ class SlimRouter
'classname' => 'AbstractPetApi',
'userClassname' => 'PetApi',
'operationId' => 'deletePet',
'responses' => [
'default' => [
'code' => 200,
'message' => 'successful operation',
'jsonSchema' => '{
"description" : "successful operation",
"content" : { }
}',
],
'400' => [
'code' => 400,
'message' => 'Invalid pet value',
'jsonSchema' => '{
"description" : "Invalid pet value",
"content" : { }
}',
],
],
'authMethods' => [
// oauth2 security schema named 'petstore_auth'
[
@@ -369,6 +729,43 @@ class SlimRouter
'classname' => 'AbstractPetApi',
'userClassname' => 'PetApi',
'operationId' => 'getPetById',
'responses' => [
'default' => [
'code' => 200,
'message' => 'successful operation',
'jsonSchema' => '{
"description" : "successful operation",
"content" : {
"application/xml" : {
"schema" : {
"$ref" : "#/components/schemas/Pet"
}
},
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/Pet"
}
}
}
}',
],
'400' => [
'code' => 400,
'message' => 'Invalid ID supplied',
'jsonSchema' => '{
"description" : "Invalid ID supplied",
"content" : { }
}',
],
'404' => [
'code' => 404,
'message' => 'Pet not found',
'jsonSchema' => '{
"description" : "Pet not found",
"content" : { }
}',
],
],
'authMethods' => [
// apiKey security schema named 'api_key'
[
@@ -392,6 +789,16 @@ class SlimRouter
'classname' => 'AbstractPetApi',
'userClassname' => 'PetApi',
'operationId' => 'updatePetWithForm',
'responses' => [
'405' => [
'code' => 405,
'message' => 'Invalid input',
'jsonSchema' => '{
"description" : "Invalid input",
"content" : { }
}',
],
],
'authMethods' => [
// oauth2 security schema named 'petstore_auth'
[
@@ -415,6 +822,22 @@ class SlimRouter
'classname' => 'AbstractPetApi',
'userClassname' => 'PetApi',
'operationId' => 'uploadFile',
'responses' => [
'default' => [
'code' => 200,
'message' => 'successful operation',
'jsonSchema' => '{
"description" : "successful operation",
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/ApiResponse"
}
}
}
}',
],
],
'authMethods' => [
// oauth2 security schema named 'petstore_auth'
[
@@ -438,6 +861,22 @@ class SlimRouter
'classname' => 'AbstractPetApi',
'userClassname' => 'PetApi',
'operationId' => 'uploadFileWithRequiredFile',
'responses' => [
'default' => [
'code' => 200,
'message' => 'successful operation',
'jsonSchema' => '{
"description" : "successful operation",
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/ApiResponse"
}
}
}
}',
],
],
'authMethods' => [
// oauth2 security schema named 'petstore_auth'
[
@@ -461,6 +900,26 @@ class SlimRouter
'classname' => 'AbstractStoreApi',
'userClassname' => 'StoreApi',
'operationId' => 'getInventory',
'responses' => [
'default' => [
'code' => 200,
'message' => 'successful operation',
'jsonSchema' => '{
"description" : "successful operation",
"content" : {
"application/json" : {
"schema" : {
"type" : "object",
"additionalProperties" : {
"type" : "integer",
"format" : "int32"
}
}
}
}
}',
],
],
'authMethods' => [
// apiKey security schema named 'api_key'
[
@@ -484,6 +943,35 @@ class SlimRouter
'classname' => 'AbstractStoreApi',
'userClassname' => 'StoreApi',
'operationId' => 'placeOrder',
'responses' => [
'default' => [
'code' => 200,
'message' => 'successful operation',
'jsonSchema' => '{
"description" : "successful operation",
"content" : {
"application/xml" : {
"schema" : {
"$ref" : "#/components/schemas/Order"
}
},
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/Order"
}
}
}
}',
],
'400' => [
'code' => 400,
'message' => 'Invalid Order',
'jsonSchema' => '{
"description" : "Invalid Order",
"content" : { }
}',
],
],
'authMethods' => [
],
],
@@ -495,6 +983,24 @@ class SlimRouter
'classname' => 'AbstractStoreApi',
'userClassname' => 'StoreApi',
'operationId' => 'deleteOrder',
'responses' => [
'400' => [
'code' => 400,
'message' => 'Invalid ID supplied',
'jsonSchema' => '{
"description" : "Invalid ID supplied",
"content" : { }
}',
],
'404' => [
'code' => 404,
'message' => 'Order not found',
'jsonSchema' => '{
"description" : "Order not found",
"content" : { }
}',
],
],
'authMethods' => [
],
],
@@ -506,6 +1012,43 @@ class SlimRouter
'classname' => 'AbstractStoreApi',
'userClassname' => 'StoreApi',
'operationId' => 'getOrderById',
'responses' => [
'default' => [
'code' => 200,
'message' => 'successful operation',
'jsonSchema' => '{
"description" : "successful operation",
"content" : {
"application/xml" : {
"schema" : {
"$ref" : "#/components/schemas/Order"
}
},
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/Order"
}
}
}
}',
],
'400' => [
'code' => 400,
'message' => 'Invalid ID supplied',
'jsonSchema' => '{
"description" : "Invalid ID supplied",
"content" : { }
}',
],
'404' => [
'code' => 404,
'message' => 'Order not found',
'jsonSchema' => '{
"description" : "Order not found",
"content" : { }
}',
],
],
'authMethods' => [
],
],
@@ -517,6 +1060,16 @@ class SlimRouter
'classname' => 'AbstractUserApi',
'userClassname' => 'UserApi',
'operationId' => 'createUser',
'responses' => [
'default' => [
'code' => 0,
'message' => 'successful operation',
'jsonSchema' => '{
"description" : "successful operation",
"content" : { }
}',
],
],
'authMethods' => [
],
],
@@ -528,6 +1081,16 @@ class SlimRouter
'classname' => 'AbstractUserApi',
'userClassname' => 'UserApi',
'operationId' => 'createUsersWithArrayInput',
'responses' => [
'default' => [
'code' => 0,
'message' => 'successful operation',
'jsonSchema' => '{
"description" : "successful operation",
"content" : { }
}',
],
],
'authMethods' => [
],
],
@@ -539,6 +1102,16 @@ class SlimRouter
'classname' => 'AbstractUserApi',
'userClassname' => 'UserApi',
'operationId' => 'createUsersWithListInput',
'responses' => [
'default' => [
'code' => 0,
'message' => 'successful operation',
'jsonSchema' => '{
"description" : "successful operation",
"content" : { }
}',
],
],
'authMethods' => [
],
],
@@ -550,6 +1123,51 @@ class SlimRouter
'classname' => 'AbstractUserApi',
'userClassname' => 'UserApi',
'operationId' => 'loginUser',
'responses' => [
'default' => [
'code' => 200,
'message' => 'successful operation',
'jsonSchema' => '{
"description" : "successful operation",
"headers" : {
"X-Rate-Limit" : {
"description" : "calls per hour allowed by the user",
"schema" : {
"type" : "integer",
"format" : "int32"
}
},
"X-Expires-After" : {
"description" : "date in UTC when token expires",
"schema" : {
"type" : "string",
"format" : "date-time"
}
}
},
"content" : {
"application/xml" : {
"schema" : {
"type" : "string"
}
},
"application/json" : {
"schema" : {
"type" : "string"
}
}
}
}',
],
'400' => [
'code' => 400,
'message' => 'Invalid username/password supplied',
'jsonSchema' => '{
"description" : "Invalid username/password supplied",
"content" : { }
}',
],
],
'authMethods' => [
],
],
@@ -561,6 +1179,16 @@ class SlimRouter
'classname' => 'AbstractUserApi',
'userClassname' => 'UserApi',
'operationId' => 'logoutUser',
'responses' => [
'default' => [
'code' => 0,
'message' => 'successful operation',
'jsonSchema' => '{
"description" : "successful operation",
"content" : { }
}',
],
],
'authMethods' => [
],
],
@@ -572,6 +1200,24 @@ class SlimRouter
'classname' => 'AbstractUserApi',
'userClassname' => 'UserApi',
'operationId' => 'deleteUser',
'responses' => [
'400' => [
'code' => 400,
'message' => 'Invalid username supplied',
'jsonSchema' => '{
"description" : "Invalid username supplied",
"content" : { }
}',
],
'404' => [
'code' => 404,
'message' => 'User not found',
'jsonSchema' => '{
"description" : "User not found",
"content" : { }
}',
],
],
'authMethods' => [
],
],
@@ -583,6 +1229,43 @@ class SlimRouter
'classname' => 'AbstractUserApi',
'userClassname' => 'UserApi',
'operationId' => 'getUserByName',
'responses' => [
'default' => [
'code' => 200,
'message' => 'successful operation',
'jsonSchema' => '{
"description" : "successful operation",
"content" : {
"application/xml" : {
"schema" : {
"$ref" : "#/components/schemas/User"
}
},
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/User"
}
}
}
}',
],
'400' => [
'code' => 400,
'message' => 'Invalid username supplied',
'jsonSchema' => '{
"description" : "Invalid username supplied",
"content" : { }
}',
],
'404' => [
'code' => 404,
'message' => 'User not found',
'jsonSchema' => '{
"description" : "User not found",
"content" : { }
}',
],
],
'authMethods' => [
],
],
@@ -594,6 +1277,24 @@ class SlimRouter
'classname' => 'AbstractUserApi',
'userClassname' => 'UserApi',
'operationId' => 'updateUser',
'responses' => [
'400' => [
'code' => 400,
'message' => 'Invalid user supplied',
'jsonSchema' => '{
"description" : "Invalid user supplied",
"content" : { }
}',
],
'404' => [
'code' => 404,
'message' => 'User not found',
'jsonSchema' => '{
"description" : "User not found",
"content" : { }
}',
],
],
'authMethods' => [
],
],
@@ -631,12 +1332,13 @@ class SlimRouter
throw new Exception($message);
};
$userOptions = null;
if ($settings instanceof ContainerInterface && $settings->has('tokenAuthenticationOptions')) {
$userOptions = $settings->get('tokenAuthenticationOptions');
} elseif (is_array($settings) && isset($settings['tokenAuthenticationOptions'])) {
$userOptions = $settings['tokenAuthenticationOptions'];
}
$userOptions = $this->getSetting($settings, 'tokenAuthenticationOptions', null);
// mocker options
$mockerOptions = $this->getSetting($settings, 'mockerOptions', null);
$dataMocker = $mockerOptions['dataMocker'] ?? new OpenApiDataMocker();
$getMockResponseCallback = $mockerOptions['getMockResponseCallback'] ?? null;
$mockAfterCallback = $mockerOptions['afterCallback'] ?? null;
foreach ($this->operations as $operation) {
$callback = function ($request, $response, $arguments) use ($operation) {
@@ -703,6 +1405,10 @@ class SlimRouter
}
}
if (is_callable($getMockResponseCallback)) {
$middlewares[] = new OpenApiDataMockerMiddleware($dataMocker, $operation['responses'], $getMockResponseCallback, $mockAfterCallback);
}
$this->addRoute(
[$operation['httpMethod']],
"{$operation['basePathWithoutHost']}{$operation['path']}",
@@ -729,6 +1435,26 @@ class SlimRouter
return array_merge($userOptions, $staticOptions);
}
/**
* Returns app setting by name.
*
* @param ContainerInterface|array $settings Either a ContainerInterface or an associative array of app settings
* @param string $settingName Setting name
* @param mixed $default Default setting value.
*
* @return mixed
*/
private function getSetting($settings, $settingName, $default = null)
{
if ($settings instanceof ContainerInterface && $settings->has($settingName)) {
return $settings->get($settingName);
} elseif (is_array($settings) && array_key_exists($settingName, $settings)) {
return $settings[$settingName];
}
return $default;
}
/**
* Add route with multiple methods
*

View File

@@ -0,0 +1,273 @@
<?php
/**
* OpenApiDataMockerMiddlewareTest
*
* PHP version 7.1
*
* @package OpenAPIServer
* @author OpenAPI Generator team
* @link https://github.com/openapitools/openapi-generator
*/
/**
* OpenAPIServer
*
* This spec is mainly for testing Petstore server and contains fake endpoints, models. Please do not use this for any other purpose. Special characters: \" \\
* The version of the OpenAPI document: 1.0.0
* Generated by: https://github.com/openapitools/openapi-generator.git
*/
/**
* NOTE: This class is auto generated by the openapi generator program.
* https://github.com/openapitools/openapi-generator
* Do not edit the class manually.
*/
namespace OpenAPIServer\Mock;
use OpenAPIServer\Mock\OpenApiDataMockerMiddleware;
use OpenAPIServer\Mock\OpenApiDataMocker;
use Slim\Factory\AppFactory;
use Slim\Factory\ServerRequestCreatorFactory;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use PHPUnit\Framework\TestCase;
use StdClass;
/**
* OpenApiDataMockerMiddlewareTest Class Doc Comment
*
* @package OpenAPIServer\Mock
* @author OpenAPI Generator team
* @link https://github.com/openapitools/openapi-generator
* @coversDefaultClass \OpenAPIServer\Mock\OpenApiDataMockerMiddleware
*/
class OpenApiDataMockerMiddlewareTest extends TestCase
{
/**
* @covers ::__construct
* @dataProvider provideConstructCorrectArguments
*/
public function testConstructor(
$mocker,
$responses,
$getMockResponseCallback,
$afterCallback
) {
$middleware = new OpenApiDataMockerMiddleware($mocker, $responses, $getMockResponseCallback, $afterCallback);
$this->assertInstanceOf(OpenApiDataMockerMiddleware::class, $middleware);
$this->assertNotNull($middleware);
}
public function provideConstructCorrectArguments()
{
$getMockResponseCallback = function () {
return false;
};
$afterCallback = function () {
return false;
};
return [
[new OpenApiDataMocker(), [], null, null],
[new OpenApiDataMocker(), [], $getMockResponseCallback, $afterCallback],
];
}
/**
* @covers ::__construct
* @dataProvider provideConstructInvalidArguments
* @expectedException \InvalidArgumentException
* @expectedException \TypeError
*/
public function testConstructorWithInvalidArguments(
$mocker,
$responses,
$getMockResponseCallback,
$afterCallback
) {
$middleware = new OpenApiDataMockerMiddleware($mocker, $responses, $getMockResponseCallback, $afterCallback);
}
public function provideConstructInvalidArguments()
{
return [
'getMockResponseCallback not callable' => [
new OpenApiDataMocker(), [], 'foobar', null,
],
'afterCallback not callable' => [
new OpenApiDataMocker(), [], null, 'foobar',
],
];
}
/**
* @covers ::process
* @dataProvider provideProcessArguments
*/
public function testProcess(
$mocker,
$responses,
$getMockResponseCallback,
$afterCallback,
$request,
$expectedStatusCode,
$expectedHeaders,
$notExpectedHeaders,
$expectedBody
) {
// Create a stub for the RequestHandlerInterface interface.
$handler = $this->createMock(RequestHandlerInterface::class);
$handler->method('handle')
->willReturn(AppFactory::determineResponseFactory()->createResponse());
$middleware = new OpenApiDataMockerMiddleware(
$mocker,
$responses,
$getMockResponseCallback,
$afterCallback
);
$response = $middleware->process($request, $handler);
// check status code
$this->assertSame($expectedStatusCode, $response->getStatusCode());
// check http headers in request
foreach ($expectedHeaders as $expectedHeader => $expectedHeaderValue) {
$this->assertTrue($response->hasHeader($expectedHeader));
if ($expectedHeaderValue !== '*') {
$this->assertSame($expectedHeaderValue, $response->getHeader($expectedHeader)[0]);
}
}
foreach ($notExpectedHeaders as $notExpectedHeader) {
$this->assertFalse($response->hasHeader($notExpectedHeader));
}
// check body
if (is_array($expectedBody)) {
// random values, check keys only
foreach ($expectedBody as $attribute => $value) {
$this->assertObjectHasAttribute($attribute, json_decode((string) $response->getBody(), false));
}
} else {
$this->assertEquals($expectedBody, (string) $response->getBody());
}
}
public function provideProcessArguments()
{
$mocker = new OpenApiDataMocker();
$isMockResponseRequired = function (ServerRequestInterface $request) {
$mockHttpHeader = 'X-OpenAPIServer-Mock';
return $request->hasHeader($mockHttpHeader)
&& $request->getHeader($mockHttpHeader)[0] === 'ping';
};
$getMockResponseCallback = function (ServerRequestInterface $request, array $responses) use ($isMockResponseRequired) {
if ($isMockResponseRequired($request)) {
if (array_key_exists('default', $responses)) {
return $responses['default'];
}
// return first response
return $responses[array_key_first($responses)];
}
return false;
};
$afterCallback = function ($request, $response) use ($isMockResponseRequired) {
if ($isMockResponseRequired($request)) {
$response = $response->withHeader('X-OpenAPIServer-Mock', 'pong');
}
return $response;
};
$responses = [
'400' => [
'code' => 400,
'jsonSchema' => json_encode([
'description' => 'Bad Request Response',
'content' => new StdClass(),
]),
],
'default' => [
'code' => 201,
'jsonSchema' => json_encode([
'description' => 'Success Response',
'headers' => [
'X-Location' => ['schema' => ['type' => 'string']],
'X-Created-Id' => ['schema' => ['type' => 'integer']],
],
'content' => [
'application/json;encoding=utf-8' => ['schema' => ['type' => 'object', 'properties' => ['id' => ['type' => 'integer'], 'className' => ['type' => 'string'], 'declawed' => ['type' => 'boolean']]]],
],
]),
],
];
$responsesXmlOnly = [
'default' => [
'code' => 201,
'jsonSchema' => json_encode([
'description' => 'Success Response',
'content' => [
'application/xml' => [
'schema' => [
'type' => 'string',
],
],
],
]),
],
];
$requestFactory = ServerRequestCreatorFactory::create();
return [
'callbacks null' => [
$mocker,
$responses,
null,
null,
$requestFactory->createServerRequestFromGlobals(),
200,
[],
['X-OpenAPIServer-Mock', 'x-location', 'x-created-id'],
'',
],
'xml not supported' => [
$mocker,
$responsesXmlOnly,
$getMockResponseCallback,
$afterCallback,
$requestFactory
->createServerRequestFromGlobals()
->withHeader('X-OpenAPIServer-Mock', 'ping'),
201,
['X-OpenAPIServer-Mock' => 'pong', 'content-type' => '*/*'],
['x-location', 'x-created-id'],
'Mock feature supports only "application/json" content-type!',
],
'mock response default schema' => [
$mocker,
$responses,
$getMockResponseCallback,
$afterCallback,
$requestFactory
->createServerRequestFromGlobals()
->withHeader('X-OpenAPIServer-Mock', 'ping'),
201,
['X-OpenAPIServer-Mock' => 'pong', 'content-type' => 'application/json', 'x-location' => '*', 'x-created-id' => '*'],
[],
[
'id' => 1,
'className' => 'cat',
'declawed' => false,
],
],
];
}
}