From e4c114d85f979264db2054b7ca7bb72055ae0956 Mon Sep 17 00:00:00 2001 From: stefanorosanelli Date: Fri, 22 Sep 2017 10:44:41 +0200 Subject: [PATCH 01/20] feat: minimal open api 3 spec dynamic generation in `/tools/open-api` --- config/routes.php | 20 +++++++ src/Controller/ToolsController.php | 74 ++++++++++++++++++++++++ src/Middleware/HtmlMiddleware.php | 3 +- src/Template/Element/OpenAPI/paths.ctp | 19 ++++++ src/Template/Element/OpenAPI/schemas.ctp | 34 +++++++++++ src/Template/Layout/open_api.ctp | 1 + src/Template/OpenAPI/yaml.ctp | 19 ++++++ 7 files changed, 169 insertions(+), 1 deletion(-) create mode 100644 config/routes.php create mode 100644 src/Controller/ToolsController.php create mode 100644 src/Template/Element/OpenAPI/paths.ctp create mode 100644 src/Template/Element/OpenAPI/schemas.ctp create mode 100644 src/Template/Layout/open_api.ctp create mode 100644 src/Template/OpenAPI/yaml.ctp diff --git a/config/routes.php b/config/routes.php new file mode 100644 index 0000000..13f753d --- /dev/null +++ b/config/routes.php @@ -0,0 +1,20 @@ + '/tools', + ], + function (RouteBuilder $routes) { + // OpenAPI 3 spec in YAML format. + $routes->connect( + '/open-api', + ['controller' => 'Tools', 'action' => 'openApi', 'method' => 'GET'] + ); + + $routes->fallbacks(DashedRoute::class); + } +); diff --git a/src/Controller/ToolsController.php b/src/Controller/ToolsController.php new file mode 100644 index 0000000..e630695 --- /dev/null +++ b/src/Controller/ToolsController.php @@ -0,0 +1,74 @@ + for more details. + */ +namespace BEdita\DevTools\Controller; + +use BEdita\API\Controller\AppController as BaseController; +use Cake\Core\Configure; +use Cake\Core\Plugin; +use Cake\Event\Event; +use Cake\Routing\Router; + +/** + * Controller endpoint for `/tools` endpoint + */ +class ToolsController extends BaseController +{ + + /** + * {@inheritDoc} + * + * @codeCoverageIgnore + */ + public function initialize() + { + parent::initialize(); + + $this->Auth->getAuthorize('BEdita/API.Endpoint')->setConfig('defaultAuthorized', true); + } + + /** + * {@inheritDoc} + */ + public function beforeFilter(Event $event) + { + } + + /** + * Give suggestions for localities. + * + * @return void + */ + public function openApi() + { + $this->request->allowMethod('get'); + $this->prepareSpec(); + } + + /** + * Prepare OpenAPI v3 Yaml specification file content. + * + * @return void + */ + protected function prepareSpec() + { + $this->set('project', Configure::read('Project.name')); + $this->set('url', Router::fullBaseUrl()); + + $this->viewBuilder() + ->setPlugin('BEdita/DevTools') + ->setLayout('open_api') + ->setTemplatePath('OpenAPI') + ->setTemplate('yaml'); + //->setClassName('View'); + } +} diff --git a/src/Middleware/HtmlMiddleware.php b/src/Middleware/HtmlMiddleware.php index 9474cef..9456d86 100644 --- a/src/Middleware/HtmlMiddleware.php +++ b/src/Middleware/HtmlMiddleware.php @@ -38,7 +38,8 @@ class HtmlMiddleware */ public function __invoke(ServerRequestInterface $request, ResponseInterface $response, callable $next) { - if (!($request instanceof ServerRequest) || !$request->is('html')) { + $paths = explode('/', $request->url); + if (!($request instanceof ServerRequest) || !$request->is('html') || $paths[0] === 'tools') { // Not an HTML request, or unable to detect easily. return $next($request, $response); } diff --git a/src/Template/Element/OpenAPI/paths.ctp b/src/Template/Element/OpenAPI/paths.ctp new file mode 100644 index 0000000..f8939e6 --- /dev/null +++ b/src/Template/Element/OpenAPI/paths.ctp @@ -0,0 +1,19 @@ +paths: + /status: + summary: API Status + description: Service status response + security: + - apikey: [] + responses: + '200': + description: + content: + application/json: + schema: + $ref: "#/components/schemas/Status" + default: + description: Unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" diff --git a/src/Template/Element/OpenAPI/schemas.ctp b/src/Template/Element/OpenAPI/schemas.ctp new file mode 100644 index 0000000..cf083c5 --- /dev/null +++ b/src/Template/Element/OpenAPI/schemas.ctp @@ -0,0 +1,34 @@ + schemas: + + Links: + properties: + self: + type: string + home: + type: string + + Status: + properties: + links: + $ref: '#/components/schemas/Links' + meta: + properties: + status: + properties: + environment: + type: string + + Error: + properties: + error: + properties: + status: + type: string + title: + type: string + code: + type: string + description: + type: string + links: + $ref: '#/components/schemas/Links' diff --git a/src/Template/Layout/open_api.ctp b/src/Template/Layout/open_api.ctp new file mode 100644 index 0000000..a855365 --- /dev/null +++ b/src/Template/Layout/open_api.ctp @@ -0,0 +1 @@ +fetch('content') ?> diff --git a/src/Template/OpenAPI/yaml.ctp b/src/Template/OpenAPI/yaml.ctp new file mode 100644 index 0000000..ee04d77 --- /dev/null +++ b/src/Template/OpenAPI/yaml.ctp @@ -0,0 +1,19 @@ +openapi: "3.0.0" +info: + title: API + description: Build great apps with BE4 + version: "1.0.0" + +servers: + - url: + +element('OpenAPI/paths') ?> + +components: + securitySchemes: + apikey: + type: apiKey + name: server_token + in: query + +element('OpenAPI/schemas') ?> From fea15687530b34158b9b0d0a0c409a402b8833d6 Mon Sep 17 00:00:00 2001 From: stefanorosanelli Date: Fri, 8 Dec 2017 18:19:37 +0100 Subject: [PATCH 02/20] feat: spec generation with array config + templates --- config/open_api.php | 262 ++++++++++++++++++ src/Controller/ToolsController.php | 47 +++- src/Spec/OpenAPI.php | 152 ++++++++++ src/Template/Element/OpenAPI/paths.ctp | 19 -- src/Template/Element/OpenAPI/schemas.ctp | 34 --- src/Template/OpenAPI/yaml.ctp | 20 +- .../Controller/ToolsControllerTest.php | 83 ++++++ tests/TestCase/Spec/OpenAPITest.php | 86 ++++++ 8 files changed, 616 insertions(+), 87 deletions(-) create mode 100644 config/open_api.php create mode 100644 src/Spec/OpenAPI.php delete mode 100644 src/Template/Element/OpenAPI/paths.ctp delete mode 100644 src/Template/Element/OpenAPI/schemas.ctp create mode 100644 tests/TestCase/Controller/ToolsControllerTest.php create mode 100644 tests/TestCase/Spec/OpenAPITest.php diff --git a/config/open_api.php b/config/open_api.php new file mode 100644 index 0000000..9b25cbc --- /dev/null +++ b/config/open_api.php @@ -0,0 +1,262 @@ + [ + 'openapi' => '3.0.0', + 'info' => [ + 'title' => '', // will be 'project name' + 'description' => 'Auto-generated specification', + 'version' => '', // will be 'BEdita version' + ], + 'servers' => [ + [ + 'url' => '', // will be 'project base URL' + ], + ], + 'paths' => [ + '/status' => [ + 'get' => [ + 'summary' => 'API Status', + 'description' => 'Service status response', + 'responses' => [ + '200' => [ + 'description' => '', + 'content' => [ + 'application/json' => [ + 'schema' => [ + '$ref' => '#/components/schemas/status' + ], + ] + ], + ], + '401' => [ + '$ref' => '#/components/responses/401', + ], + ], + ], + ], + '/home' => [ + 'get' => [ + 'summary' => 'API Home page', + 'description' => 'Information on avilable endpoints and methods', + 'responses' => [ + '200' => [ + 'description' => '', + 'content' => [ + 'application/json' => [ + 'schema' => [ + '$ref' => '#/components/schemas/home' + ], + ], + ], + ], + '401' => [ + '$ref' => '#/components/responses/401', + ], + ], + ], + ], + ], + 'components' => [ + 'securitySchemes' => [ + 'apikey' => [ + 'type' => 'apiKey', + 'name' => 'server_token', + 'in' => 'query', + ], + ], + + 'parameters' => [ + 'Id' => [ + 'name' => 'id', + 'in' => 'path', + 'description' => 'numeric resource/object id', + 'required' => true, + 'schema' => [ + 'type' => 'integer', + 'format' => 'int64', + ], + ], + ], + + 'schemas' => [ + + 'links' => [ + 'type' => 'object', + 'properties' => [ + 'self' => [ + 'type' => 'string', + ], + 'home' => [ + 'type' => 'string', + ], + ], + ], + + 'status' => [ + 'type' => 'object', + 'properties' => [ + 'links' => [ + '$ref' => '#/components/schemas/links', + ], + 'meta' => [ + 'type' => 'object', + 'properties' => [ + 'status' => [ + 'type' => 'object', + 'properties' => [ + 'environment' => [ + 'type' => 'string', + ], + ], + ], + ], + ], + ], + ], + + 'home' => [ + 'type' => 'object', + 'properties' => [ + 'links' => [ + '$ref' => '#/components/schemas/links', + ], + 'meta' => [ + 'type' => 'object', + 'properties' => [ + 'resources' => [ + 'type' => 'array', + 'items' => [ + 'type' => 'object', + ] + ], + 'project' => [ + 'properties' => [ + 'name' => [ + 'type' => 'string', + ], + ], + ], + 'version' => [ + 'type' => 'string', + ], + ], + ], + ], + ], + + 'error' => [ + 'properties' => [ + 'error' => [ + 'properties' => [ + 'status' => [ + 'type' => 'string', + ], + 'title' => [ + 'type' => 'string', + ], + 'code' => [ + 'type' => 'string', + ], + 'description' => [ + 'type' => 'string', + ], + ], + ], + 'links' => [ + '$ref' => '#/components/schemas/links', + ], + ], + ], + ], + + 'responses' => [ + '401' => [ + 'description' => 'Authorization information is missing or invalid', + 'content' => [ + 'application/json' => [ + 'schema' => [ + '$ref' => '#/components/schemas/error' + ], + ], + ], + ], + ], + ], + ], + + 'OATemplates' => [ + 'paths' => [ + '/{$resource}/{id}' => [ + 'get' => [ + 'summary' => '', + 'description' => '', + 'parameters' => [ + [ + '$ref' => '#/components/parameters/Id', + ], + ], + 'responses' => [ + '200' => [ + 'description' => 'Retrieve single "{$resource}"', + 'content' => [ + 'application/json' => [ + 'schema' => [ + '$ref' => '#/components/schemas/{$resource}' + ], + ] + ], + ], + '401' => [ + '$ref' => '#/components/responses/401', + ], + ], + ], + ], + ], + 'components' => [ + 'schemas' => [ + '{$resource}' => [ + 'type' => 'object', + 'properties' => [ + 'data' => [ + 'type' => 'object', + 'properties' => [ + 'id' => [ + 'type' => 'string', + ], + 'type' => [ + 'type' => 'string', + ], + 'attributes' => [ + 'type' => 'object', + 'properties' => [ + '$ref' => '#/components/schemas/{$resource}_attributes', + ], + ], + 'meta' => [ + 'type' => 'object', + 'properties' => [ + '$ref' => '#/components/schemas/{$resource}_meta', + ], + ], + 'relationships' => [ + 'type' => 'object', + 'properties' => [ + '$ref' => '#/components/schemas/{$resource}_relationships', + ], + ], + ] + ], + 'links' => [ + '$ref' => '#/components/schemas/links', + ], + ] + ], + ], + ], + ], + +]; diff --git a/src/Controller/ToolsController.php b/src/Controller/ToolsController.php index e630695..7bd28b9 100644 --- a/src/Controller/ToolsController.php +++ b/src/Controller/ToolsController.php @@ -13,16 +13,24 @@ namespace BEdita\DevTools\Controller; use BEdita\API\Controller\AppController as BaseController; +use BEdita\DevTools\Spec\OpenAPI; use Cake\Core\Configure; use Cake\Core\Plugin; use Cake\Event\Event; use Cake\Routing\Router; +use Zend\Diactoros\Stream; /** * Controller endpoint for `/tools` endpoint */ class ToolsController extends BaseController { + /** + * YAML content type. + * + * @var string + */ + const YAML_CONTENT_TYPE = 'application/x-yaml'; /** * {@inheritDoc} @@ -34,41 +42,50 @@ public function initialize() parent::initialize(); $this->Auth->getAuthorize('BEdita/API.Endpoint')->setConfig('defaultAuthorized', true); + + if ($this->components()->has('JsonApi')) { + $this->components()->unload('JsonApi'); + } } /** * {@inheritDoc} + * + * @codeCoverageIgnore */ public function beforeFilter(Event $event) { } /** - * Give suggestions for localities. + * Return OpenAPI spec in JSON or YAML * - * @return void + * @return \Cake\Http\Response */ public function openApi() { $this->request->allowMethod('get'); - $this->prepareSpec(); - } - /** - * Prepare OpenAPI v3 Yaml specification file content. - * - * @return void - */ - protected function prepareSpec() - { - $this->set('project', Configure::read('Project.name')); - $this->set('url', Router::fullBaseUrl()); + if ($this->request->is('json')) { + $this->viewBuilder()->setClassName('Json'); + $this->set(OpenAPI::generate()); + $this->set('_serialize', true); + return $this->render(); + } + + $this->set('yaml', OpenAPI::generateYaml()); $this->viewBuilder() ->setPlugin('BEdita/DevTools') ->setLayout('open_api') ->setTemplatePath('OpenAPI') - ->setTemplate('yaml'); - //->setClassName('View'); + ->setTemplate('yaml') + ->setClassName('View'); + + if ($this->request->is('html')) { + return $this->render(); + } + + return $this->render()->withType(static::YAML_CONTENT_TYPE); } } diff --git a/src/Spec/OpenAPI.php b/src/Spec/OpenAPI.php new file mode 100644 index 0000000..4e23547 --- /dev/null +++ b/src/Spec/OpenAPI.php @@ -0,0 +1,152 @@ + for more details. + */ + +namespace BEdita\DevTools\Spec; + +use BEdita\Core\Model\Schema\JsonSchema; +use Cake\Core\Configure; +use Cake\Routing\Router; +use Cake\Utility\Hash; +use Symfony\Component\Yaml\Yaml; + +/** + * OpenAPI generation utility methods. + * + * @since 4.0.0 + */ +class OpenAPI +{ + /** + * Available resource and object types described by the spec + * + * @var array + */ + protected static $types = []; + + /** + * Generate OpenAPI v3 project as PHP array. + * + * @return array + */ + public static function generate() + { + Configure::load('BEdita/DevTools.open_api'); + $spec = Configure::read('OpenAPI'); + + $spec['info']['title'] = Configure::read('Project.name') . ' API'; + $spec['info']['version'] = Configure::read('BEdita.version'); + $spec['servers'][0]['url'] = Router::fullBaseUrl(); + + // add dynamic paths and components for resources/objects + $spec['paths'] = array_merge($spec['paths'], static::dynamicPaths()); + $spec['components']['schemas'] = array_merge($spec['components']['schemas'], static::dynamicSchemas()); + + return $spec; + } + + /** + * Generate OpenAPI v3 project as YAML string. + * + * @return string OpenAPI spec in YAML format + */ + public static function generateYaml() + { + $spec = static::generate(); + + // Using 20 as 'level where you switch to inline YAML' just to put a `high` number + // and 4 as indentation spaces + return Yaml::dump($spec, 20, 4, Yaml::DUMP_EMPTY_ARRAY_AS_SEQUENCE); + } + + /** + * Add dynamic `paths` from template + * + * @return array OpenAPI dynamic paths + */ + protected static function dynamicPaths() + { + $res = []; + $paths = Configure::read('OATemplates.paths'); + $types = static::availableTypes(); + foreach ($types as $t) { + foreach ($paths as $url => $data) { + $path = str_replace('{$resource}', $t, $url); + $data = json_decode(str_replace('{$resource}', $t, json_encode($data)), true); + $res[$path] = $data; + } + } + + return $res; + } + + /** + * Available types + * + * @return array OpenAPI dynamic paths + */ + protected static function availableTypes() + { + if (empty(static::$types)) { + static::$types = ['roles', 'objects', 'users']; + } + + return static::$types; + } + + /** + * Add dynamic `components.schemas` from template + * + * @return array OpenAPI dynamic schemas + */ + protected static function dynamicSchemas() + { + $res = []; + $schemas = Configure::read('OATemplates.components.schemas'); + $types = static::availableTypes(); + foreach ($types as $t) { + $template = json_decode(str_replace('{$resource}', $t, json_encode($schemas['{$resource}'])), true); + $res[$t] = $template; + $schema = static::retrieveSchema($t); + foreach (['attributes', 'meta', 'relationships'] as $item) { + $res[sprintf('%s_%s', $t, $item)] = $schema[$item]; + } + } + + return $res; + } + + /** + * Get type schema array: attributes, meta and relationships + * + * @param string $type Input resource or object type + * @return array OpenAPI dynamic components + */ + protected static function retrieveSchema($type) + { + $schema = JsonSchema::typeSchema($type); + + $attributes = $meta = ['type' => 'object', 'properties' => []]; + $relationships = []; + + foreach ($schema['properties'] as $name => $data) { + if (!empty($data['isMeta'])) { + unset($data['isMeta']); + $meta['properties'][$name] = $data; + } else { + $attributes['properties'][$name] = $data; + } + } + + return compact('attributes', 'meta', 'relationships'); + } +} diff --git a/src/Template/Element/OpenAPI/paths.ctp b/src/Template/Element/OpenAPI/paths.ctp deleted file mode 100644 index f8939e6..0000000 --- a/src/Template/Element/OpenAPI/paths.ctp +++ /dev/null @@ -1,19 +0,0 @@ -paths: - /status: - summary: API Status - description: Service status response - security: - - apikey: [] - responses: - '200': - description: - content: - application/json: - schema: - $ref: "#/components/schemas/Status" - default: - description: Unexpected error - content: - application/json: - schema: - $ref: "#/components/schemas/Error" diff --git a/src/Template/Element/OpenAPI/schemas.ctp b/src/Template/Element/OpenAPI/schemas.ctp deleted file mode 100644 index cf083c5..0000000 --- a/src/Template/Element/OpenAPI/schemas.ctp +++ /dev/null @@ -1,34 +0,0 @@ - schemas: - - Links: - properties: - self: - type: string - home: - type: string - - Status: - properties: - links: - $ref: '#/components/schemas/Links' - meta: - properties: - status: - properties: - environment: - type: string - - Error: - properties: - error: - properties: - status: - type: string - title: - type: string - code: - type: string - description: - type: string - links: - $ref: '#/components/schemas/Links' diff --git a/src/Template/OpenAPI/yaml.ctp b/src/Template/OpenAPI/yaml.ctp index ee04d77..1701d38 100644 --- a/src/Template/OpenAPI/yaml.ctp +++ b/src/Template/OpenAPI/yaml.ctp @@ -1,19 +1 @@ -openapi: "3.0.0" -info: - title: API - description: Build great apps with BE4 - version: "1.0.0" - -servers: - - url: - -element('OpenAPI/paths') ?> - -components: - securitySchemes: - apikey: - type: apiKey - name: server_token - in: query - -element('OpenAPI/schemas') ?> + diff --git a/tests/TestCase/Controller/ToolsControllerTest.php b/tests/TestCase/Controller/ToolsControllerTest.php new file mode 100644 index 0000000..ed92515 --- /dev/null +++ b/tests/TestCase/Controller/ToolsControllerTest.php @@ -0,0 +1,83 @@ + for more details. + */ + +namespace BEdita\DevTools\Test\TestCase\Controller; + +use BEdita\API\TestSuite\IntegrationTestCase; + +/** + * {@see \BEdita\DevTools\Controller\ToolsController} Test Case + * + * @coversDefaultClass \BEdita\DevTools\Controller\ToolsController + */ +class ToolsControllerTest extends IntegrationTestCase +{ + /** + * Fixtures + * + * @var array + */ + public $fixtures = [ + 'plugin.BEdita/Core.media', + 'plugin.BEdita/Core.locations', + 'plugin.BEdita/Core.property_types', + 'plugin.BEdita/Core.properties', + ]; + + /** + * Data provider for `testOpenAPI` + * + * @return array + */ + public function openAPIProvider() + { + return [ + 'json' => [ + 'application/json', + 'application/json', + ], + 'browser' => [ + 'text/html', + 'text/html', + ], + 'yaml' => [ + 'application/x-yaml', + '*/*' + ], + ]; + } + + /** + * Test `openApi` method. + * + * @param string $expected Expected response content type + * @param string $accept Accept request header + * @return void + * + * @covers ::openApi() + * @covers ::initialize() + * @dataProvider openAPIProvider + */ + public function testOpenAPI($expected, $accept = '') + { + $headers = empty($accept) ? [] : ['Accept' => $accept]; + $this->configRequestHeaders('GET', $headers); + $this->get('/tools/open-api'); + + $body = (string)$this->_response->getBody(); + static::assertNotEmpty($body); + + $this->assertResponseCode(200); + $this->assertContentType($expected); + } +} diff --git a/tests/TestCase/Spec/OpenAPITest.php b/tests/TestCase/Spec/OpenAPITest.php new file mode 100644 index 0000000..35e66a5 --- /dev/null +++ b/tests/TestCase/Spec/OpenAPITest.php @@ -0,0 +1,86 @@ + for more details. + */ +namespace BEdita\Core\Test\TestCase\Utility; + +use BEdita\DevTools\Spec\OpenAPI; +use Cake\Core\Configure; +use Cake\TestSuite\TestCase; +use Symfony\Component\Yaml\Yaml; + +/** + * {@see \BEdita\DevTools\Spec\OpenAPI} Test Case + * + * @coversDefaultClass \BEdita\DevTools\Spec\OpenAPI + */ +class OpenAPITest extends TestCase +{ + + /** + * Fixtures + * + * @var array + */ + public $fixtures = [ + 'plugin.BEdita/Core.object_types', + 'plugin.BEdita/Core.property_types', + 'plugin.BEdita/Core.properties', + 'plugin.BEdita/Core.objects', + 'plugin.BEdita/Core.locations', + 'plugin.BEdita/Core.media', + 'plugin.BEdita/Core.profiles', + 'plugin.BEdita/Core.users', + 'plugin.BEdita/Core.relations', + 'plugin.BEdita/Core.roles', + 'plugin.BEdita/Core.relation_types', + 'plugin.BEdita/Core.streams', + ]; + + /** + * Test `generate` method + * + * @return void + * + * @covers ::generate() + * @covers ::dynamicPaths() + * @covers ::dynamicSchemas() + * @covers ::availableTypes() + * @covers ::retrieveSchema() + */ + public function testGenerate() + { + $result = OpenAPI::generate(); + + static::assertNotEmpty($result); + $expectedKeys = ['openapi', 'info', 'servers', 'paths', 'components']; + + static::assertEquals($expectedKeys, array_keys($result)); + } + + /** + * Test `generateYaml` method + * + * @return void + * + * @covers ::generateYaml() + */ + public function testGenerateYaml() + { + $result = OpenAPI::generateYaml(); + + static::assertNotEmpty($result); + + $data = Yaml::parse($result); + $expectedKeys = ['openapi', 'info', 'servers', 'paths', 'components']; + static::assertEquals($expectedKeys, array_keys($data)); + } +} From 82c58fdb77ef5de80a6aa33f84a45d28a7202769 Mon Sep 17 00:00:00 2001 From: stefanorosanelli Date: Fri, 8 Dec 2017 18:20:03 +0100 Subject: [PATCH 03/20] chore: introduce api/core dependencies --- composer.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index b9a4af6..35cad86 100644 --- a/composer.json +++ b/composer.json @@ -21,8 +21,9 @@ } ], "require": { - "cakephp/cakephp": ">=3.4.4, <4.0", - "cakephp/debug_kit": "~3.2 !=3.5.1" + "bedita/core": "dev-4-cactus", + "bedita/api": "dev-4-cactus", + "cakephp/debug_kit": "~3.10" }, "require-dev": { "phpunit/phpunit": "*", From cf17c0099df4873537000ef7af9032d000dd694e Mon Sep 17 00:00:00 2001 From: stefanorosanelli Date: Fri, 8 Dec 2017 18:46:39 +0100 Subject: [PATCH 04/20] test: allow BE integration tests against`API` and `Core` plugins --- tests/bootstrap.php | 206 +++++++++++++++++++++------- tests/test_app/config/app.php | 160 +++++++++++++++++++++ tests/test_app/config/bootstrap.php | 2 + tests/test_app/config/routes.php | 15 +- tests/test_app/src/Application.php | 52 +++++++ 5 files changed, 373 insertions(+), 62 deletions(-) create mode 100644 tests/test_app/config/app.php create mode 100644 tests/test_app/config/bootstrap.php create mode 100644 tests/test_app/src/Application.php diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 32904d4..cc3e90e 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -11,18 +11,24 @@ * See LICENSE.LGPL or for more details. */ +use BEdita\Core\Filesystem\FilesystemRegistry; +use BEdita\Core\Plugin; use Cake\Cache\Cache; -use Cake\Core\ClassLoader; +use Cake\Console\ConsoleErrorHandler; use Cake\Core\Configure; -use Cake\Core\Plugin; +use Cake\Core\Configure\Engine\PhpConfig; +use Cake\Database\Type; use Cake\Datasource\ConnectionManager; +use Cake\Error\ErrorHandler; +use Cake\Http\ServerRequest; +use Cake\Log\Log; +use Cake\Mailer\Email; +use Cake\Utility\Security; + +require_once 'vendor/autoload.php'; // Path constants to a few helpful things. -if (!defined('DS')) { - define('DS', DIRECTORY_SEPARATOR); -} define('ROOT', dirname(__DIR__) . DS); -require_once ROOT . 'vendor' . DS . 'autoload.php'; define('CAKE_CORE_INCLUDE_PATH', ROOT . 'vendor' . DS . 'cakephp' . DS . 'cakephp'); define('CORE_PATH', ROOT . 'vendor' . DS . 'cakephp' . DS . 'cakephp' . DS); define('CAKE', CORE_PATH . 'src' . DS); @@ -36,61 +42,161 @@ define('CACHE', TMP); define('LOGS', TMP); -$loader = new ClassLoader(); +$loader = new \Cake\Core\ClassLoader; $loader->register(); -$loader->addNamespace('TestApp', APP); - -require_once CORE_PATH . 'config' . DS . 'bootstrap.php'; +require_once CORE_PATH . 'config/bootstrap.php'; date_default_timezone_set('UTC'); mb_internal_encoding('UTF-8'); -Configure::write('debug', true); -Configure::write('App', [ - 'namespace' => 'App', - 'encoding' => 'UTF-8', - 'base' => false, - 'baseUrl' => false, - 'dir' => 'src', - 'webroot' => 'webroot', - 'www_root' => APP . 'webroot', - 'fullBaseUrl' => 'http://api.example.org', - 'imageBaseUrl' => 'img/', - 'jsBaseUrl' => 'js/', - 'cssBaseUrl' => 'css/', - 'paths' => [ - 'plugins' => [APP . 'Plugin' . DS], - 'templates' => [APP . 'Template' . DS], - ], -]); +try { + Configure::config('default', new PhpConfig()); + Configure::load('app', 'default', false); +} catch (\Exception $e) { + exit($e->getMessage() . "\n"); +} -Cache::setConfig([ - '_cake_core_' => [ - 'engine' => 'File', - 'prefix' => 'cake_core_', - 'serialize' => true - ], - '_cake_model_' => [ - 'engine' => 'File', - 'prefix' => 'cake_model_', - 'serialize' => true - ], - 'default' => [ - 'engine' => 'File', - 'prefix' => 'default_', - 'serialize' => true - ] -]); +if (!defined('API_KEY')) { + define('API_KEY', 'API_KEY'); +} // Ensure default test connection is defined if (!getenv('db_dsn')) { - putenv('db_dsn=sqlite:///:memory:'); + putenv('db_dsn=sqlite://127.0.0.1/' . TMP . 'devtools_test.sqlite'); } -ConnectionManager::setConfig('test', [ - 'url' => getenv('db_dsn'), - 'timezone' => 'UTC', +ConnectionManager::drop('test'); +ConnectionManager::setConfig('test', ['url' => getenv('db_dsn')]); + +if (getenv('DEBUG_LOG_QUERIES')) { + ConnectionManager::get('test')->logQueries(true); + Log::setConfig('queries', [ + 'className' => 'Console', + 'stream' => 'php://stdout', + 'scopes' => ['queriesLog'], + ]); +} + +FilesystemRegistry::dropAll(); +Configure::write('Filesystem', [ + 'default' => [ + 'className' => 'BEdita/Core.Local', + 'path' => ROOT . 'vendor' . DS . 'bedita' . DS . 'core' . DS . 'tests' . DS . 'uploads', + 'baseUrl' => 'https://static.example.org/files', + ], ]); -Plugin::load('BEdita/DevTools', ['path' => ROOT, 'bootstrap' => true]); +/* When debug = true the metadata cache should last + * for a very very short time, as we want + * to refresh the cache while developers are making changes. + */ +if (Configure::read('debug')) { + Configure::write('Cache._bedita_object_types_.duration', '+2 minutes'); + Configure::write('Cache._cake_model_.duration', '+2 minutes'); + Configure::write('Cache._cake_core_.duration', '+2 minutes'); +} + +/* + * Set server timezone to UTC. You can change it to another timezone of your + * choice but using UTC makes time calculations / conversions easier. + * Check http://php.net/manual/en/timezones.php for list of valid timezone strings. + */ +date_default_timezone_set('UTC'); + +/* + * Configure the mbstring extension to use the correct encoding. + */ +mb_internal_encoding(Configure::read('App.encoding')); + +/* + * Set the default locale. This controls how dates, number and currency is + * formatted and sets the default language to use for translations. + */ +ini_set('intl.default_locale', Configure::read('App.defaultLocale')); + +/* + * Register application error and exception handlers. + */ +$isCli = PHP_SAPI === 'cli'; +if ($isCli) { + (new ConsoleErrorHandler(Configure::read('Error')))->register(); +} else { + (new ErrorHandler(Configure::read('Error')))->register(); +} + +/* + * Include the CLI bootstrap overrides. + */ +if ($isCli) { + Configure::write('Log.debug.file', 'cli-debug'); + Configure::write('Log.error.file', 'cli-error'); +} + +/* + * Set the full base URL. + * This URL is used as the base of all absolute links. + * + * If you define fullBaseUrl in your config file you can remove this. + */ +if (!Configure::read('App.fullBaseUrl')) { + $s = null; + if (env('HTTPS')) { + $s = 's'; + } + + $httpHost = env('HTTP_HOST'); + if (isset($httpHost)) { + Configure::write('App.fullBaseUrl', 'http' . $s . '://' . $httpHost); + } + unset($httpHost, $s); +} + +Cache::setConfig(Configure::consume('Cache') ?: []); +Email::setConfigTransport(Configure::consume('EmailTransport') ?: []); +Email::setConfig(Configure::consume('Email') ?: []); +Log::setConfig(Configure::consume('Log') ?: []); +Security::setSalt((string)Configure::consume('Security.salt')); +FilesystemRegistry::setConfig(Configure::consume('Filesystem') ?: []); + +ServerRequest::addDetector('mobile', function ($request) { + $detector = new \Detection\MobileDetect(); + + return $detector->isMobile(); +}); +ServerRequest::addDetector('tablet', function ($request) { + $detector = new \Detection\MobileDetect(); + + return $detector->isTablet(); +}); + +Type::build('time') + ->useImmutable() + ->useLocaleParser(); +Type::build('date') + ->useImmutable() + ->useLocaleParser(); +Type::build('datetime') + ->useImmutable() + ->useLocaleParser(); + +Cache::drop('_bedita_object_types_'); +Cache::setConfig('_bedita_object_types_', ['className' => 'Null']); +Configure::write('debug', true); + +$basePluginsPath = ROOT . 'vendor' . DS . 'bedita' . DS; + +Plugin::load( + 'BEdita/Core', + ['bootstrap' => true, 'path' => $basePluginsPath . 'core' . DS] +); + +Plugin::load( + 'BEdita/API', + ['bootstrap' => true, 'routes' => true, 'path' => $basePluginsPath . 'api' . DS] +); + +Plugin::load( + 'BEdita/DevTools', + ['routes' => true, 'path' => ROOT] +); diff --git a/tests/test_app/config/app.php b/tests/test_app/config/app.php new file mode 100644 index 0000000..c3ba544 --- /dev/null +++ b/tests/test_app/config/app.php @@ -0,0 +1,160 @@ + true, + + 'App' => [ + 'namespace' => 'BEdita\App', + 'encoding' => env('APP_ENCODING', 'UTF-8'), + 'defaultLocale' => env('APP_DEFAULT_LOCALE', 'en_US'), + 'base' => env('BEDITA_BASE_URL', false), + 'dir' => 'src', + 'webroot' => 'webroot', + 'wwwRoot' => WWW_ROOT, + // 'baseUrl' => env('SCRIPT_NAME'), + 'fullBaseUrl' => false, + 'imageBaseUrl' => 'img/', + 'cssBaseUrl' => 'css/', + 'jsBaseUrl' => 'js/', + 'paths' => [ + 'plugins' => [ROOT . 'vendor' . DS], + 'templates' => [APP . 'Template' . DS], + 'locales' => [APP . 'Locale' . DS], + ], + ], + + 'Security' => [ + 'salt' => 'super-secure-salt!', + // 'blockAnonymousApps' => true, + 'blockAnonymousUsers' => false, + ], + + 'Asset' => [ + ], + + 'Cache' => [ + 'default' => [ + 'className' => 'File', + 'path' => CACHE, + 'url' => env('CACHE_DEFAULT_URL', null), + ], + + '_cake_core_' => [ + 'className' => 'File', + 'prefix' => 'myapp_cake_core_', + 'path' => CACHE . 'persistent/', + 'serialize' => true, + 'duration' => '+1 years', + 'url' => env('CACHE_CAKECORE_URL', null), + ], + + '_cake_model_' => [ + 'className' => 'File', + 'prefix' => 'myapp_cake_model_', + 'path' => CACHE . 'models/', + 'serialize' => true, + 'duration' => '+1 years', + 'url' => env('CACHE_CAKEMODEL_URL', null), + ], + ], + + 'Error' => [ + 'errorLevel' => E_ALL, + 'exceptionRenderer' => 'BEdita\API\Error\ExceptionRenderer', + 'skipLog' => [], + 'log' => true, + 'trace' => true, + ], + + 'EmailTransport' => [ + 'default' => [ + 'className' => 'Mail', + 'host' => 'localhost', + 'port' => 25, + 'timeout' => 30, + 'username' => null, + 'password' => null, + 'client' => null, + 'tls' => null, + 'url' => env('EMAIL_TRANSPORT_DEFAULT_URL', null), + ], + ], + + 'Email' => [ + 'default' => [ + 'transport' => 'default', + 'from' => 'you@localhost', + //'charset' => 'utf-8', + //'headerCharset' => 'utf-8', + ], + ], + + 'Datasources' => [ + 'test' => [ + 'className' => 'Cake\Database\Connection', + 'driver' => 'Cake\Database\Driver\Mysql', + 'persistent' => false, + 'host' => 'localhost', + //'port' => 'non_standard_port_number', + 'username' => 'bedita', + 'password' => 'bedita', + 'database' => 'bedita_test', + 'encoding' => 'utf8', + 'timezone' => 'UTC', + 'cacheMetadata' => true, + 'quoteIdentifiers' => false, + 'log' => false, + //'init' => ['SET GLOBAL innodb_stats_on_metadata = 0'], + 'url' => env('DATABASE_TEST_URL', null), + ], + ], + + 'Log' => [ + 'debug' => [ + 'className' => 'Cake\Log\Engine\FileLog', + 'path' => LOGS, + 'file' => 'debug', + 'url' => env('LOG_DEBUG_URL', null), + 'scopes' => false, + 'levels' => ['notice', 'info', 'debug'], + ], + 'error' => [ + 'className' => 'Cake\Log\Engine\FileLog', + 'path' => LOGS, + 'file' => 'error', + 'url' => env('LOG_ERROR_URL', null), + 'scopes' => false, + 'levels' => ['warning', 'error', 'critical', 'alert', 'emergency'], + ], + 'queries' => [ + 'className' => 'Cake\Log\Engine\FileLog', + 'path' => LOGS, + 'file' => 'queries', + 'url' => env('LOG_QUERIES_URL', null), + 'scopes' => ['queriesLog'], + ], + ], + + 'Session' => [ + 'defaults' => 'php', + ], + + 'Pagination' => [ + 'limit' => 20, + 'maxLimit' => 100, + ], + + 'Project' => [ + 'name' => 'BEdita 4', + ], + + 'Signup' => [ +// 'requireActivation' => true, + ], + + 'Filesystem' => [ + 'default' => [ + 'className' => 'BEdita/Core.Local', + 'path' => WWW_ROOT . DS . 'files', + ], + ], +]; diff --git a/tests/test_app/config/bootstrap.php b/tests/test_app/config/bootstrap.php new file mode 100644 index 0000000..6abc49c --- /dev/null +++ b/tests/test_app/config/bootstrap.php @@ -0,0 +1,2 @@ + for more details. - */ +// Minimal routes file - used in tests only +use Cake\Core\Plugin; -// Do nothing. :) +Plugin::routes(); diff --git a/tests/test_app/src/Application.php b/tests/test_app/src/Application.php new file mode 100644 index 0000000..19b808a --- /dev/null +++ b/tests/test_app/src/Application.php @@ -0,0 +1,52 @@ +add(ErrorHandlerMiddleware::class) + + // Handle plugin/theme assets like CakePHP normally does. + ->add(AssetMiddleware::class) + + // Add routing middleware. + ->add(new RoutingMiddleware($this)); + + return $middlewareQueue; + } +} From 03a29063dcddccd8abc9f50ed486fcda41db04f4 Mon Sep 17 00:00:00 2001 From: stefanorosanelli Date: Fri, 8 Dec 2017 18:47:13 +0100 Subject: [PATCH 05/20] chore: travis build with sqlite only --- .travis.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 0339045..668442a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,13 +19,16 @@ jobs: include: - php: 5.6 - env: "PREFER_LOWEST=1" + env: "PREFER_LOWEST=1 DB=sqlite db_dsn='sqlite:///tmp/test.sql'" install: - composer install --prefer-dist --no-interaction - composer update --prefer-dist --prefer-lowest --no-interaction # Ensure PHPUnit is installed from source (i.e.: Git repository) otherwise a wrong version is being detected. :| - rm -rf vendor/phpunit/phpunit vendor/bin/phpunit && COMPOSER_CACHE_DIR=/dev/null composer update --prefer-source --prefer-lowest --no-interaction + - php: 7.1 + env: "DB=sqlite db_dsn='sqlite:///tmp/test.sql'" + - php: 7.0 env: "RUN=phpcs" before_script: skip From 886872c790654d5473f05e1c453f66ca9ee6beca Mon Sep 17 00:00:00 2001 From: stefanorosanelli Date: Fri, 8 Dec 2017 18:56:48 +0100 Subject: [PATCH 06/20] chore: simplified testing -temporary removed `prefer lowest` --- .travis.yml | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/.travis.yml b/.travis.yml index 668442a..ab30963 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,12 +3,6 @@ language: php dist: trusty sudo: false -php: - - 5.6 - - 7.0 - - 7.1 - - hhvm - cache: directories: - vendor @@ -18,13 +12,19 @@ jobs: fast_finish: true include: + - php: 7.0 + env: "DB=sqlite db_dsn='sqlite:///tmp/test.sql'" + - php: 5.6 - env: "PREFER_LOWEST=1 DB=sqlite db_dsn='sqlite:///tmp/test.sql'" - install: - - composer install --prefer-dist --no-interaction - - composer update --prefer-dist --prefer-lowest --no-interaction - # Ensure PHPUnit is installed from source (i.e.: Git repository) otherwise a wrong version is being detected. :| - - rm -rf vendor/phpunit/phpunit vendor/bin/phpunit && COMPOSER_CACHE_DIR=/dev/null composer update --prefer-source --prefer-lowest --no-interaction + env: "DB=sqlite db_dsn='sqlite:///tmp/test.sql'" + + # - php: 5.6 + # env: "PREFER_LOWEST=1 DB=sqlite db_dsn='sqlite:///tmp/test.sql'" + # install: + # - composer install --prefer-dist --no-interaction + # - composer update --prefer-dist --prefer-lowest --no-interaction + # # Ensure PHPUnit is installed from source (i.e.: Git repository) otherwise a wrong version is being detected. :| + # - rm -rf vendor/phpunit/phpunit vendor/bin/phpunit && COMPOSER_CACHE_DIR=/dev/null composer update --prefer-source --prefer-lowest --no-interaction - php: 7.1 env: "DB=sqlite db_dsn='sqlite:///tmp/test.sql'" From f0bc2b8edfe0918c7a12719ee73b6cf77fa5ccae Mon Sep 17 00:00:00 2001 From: stefanorosanelli Date: Fri, 8 Dec 2017 19:12:31 +0100 Subject: [PATCH 07/20] chore: core and api test classes autoload --- composer.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/composer.json b/composer.json index 35cad86..faaf9a9 100644 --- a/composer.json +++ b/composer.json @@ -38,6 +38,9 @@ "autoload-dev": { "psr-4": { "BEdita\\DevTools\\Test\\": "tests", + "BEdita\\App\\": "tests/test_app/src", + "BEdita\\Core\\Test\\": "./vendor/bedita/core/tests", + "BEdita\\API\\Test\\": "./vendor/bedita/api/tests", "Cake\\Test\\": "./vendor/cakephp/cakephp/tests" } }, From d8850ed2052e8a1a19888b07f87b2d6168156520 Mon Sep 17 00:00:00 2001 From: stefanorosanelli Date: Fri, 8 Dec 2017 19:27:55 +0100 Subject: [PATCH 08/20] chore: travis `before_script` - temporary removed code coverage --- .travis.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index ab30963..9347b71 100644 --- a/.travis.yml +++ b/.travis.yml @@ -57,9 +57,12 @@ before_install: install: # Install Composer dependencies. - composer install --prefer-dist --no-interaction - - if [ "$TRAVIS_PHP_VERSION" = 'hhvm' ]; then composer require lorenzo/multiple-iterator=~1.0; fi -script: vendor/bin/phpunit --coverage-clover=clover.xml # Run PHPUnit. +before_script: + - phpenv rehash + - set +H + +script: vendor/bin/phpunit # Run PHPUnit. after_success: bash <(curl -s https://codecov.io/bash) # Upload test coverage reports to CodeCov. From aec0dbc4da8130a3153b3059647a3a3d6975d017 Mon Sep 17 00:00:00 2001 From: stefanorosanelli Date: Fri, 8 Dec 2017 19:35:48 +0100 Subject: [PATCH 09/20] chore: add empty `tmp/` (for tests only) --- tmp/.gitkeep | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 tmp/.gitkeep diff --git a/tmp/.gitkeep b/tmp/.gitkeep new file mode 100644 index 0000000..e69de29 From 61f42c664505af1519e4cd06722d15270f4a1087 Mon Sep 17 00:00:00 2001 From: stefanorosanelli Date: Sat, 9 Dec 2017 12:37:05 +0100 Subject: [PATCH 10/20] chore: remove unused folder --- tests/Fixture/.gitkeep | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 tests/Fixture/.gitkeep diff --git a/tests/Fixture/.gitkeep b/tests/Fixture/.gitkeep deleted file mode 100644 index e69de29..0000000 From be8a00505c1e2790bcab927d3223bcda82ae6363 Mon Sep 17 00:00:00 2001 From: stefanorosanelli Date: Sat, 9 Dec 2017 12:40:29 +0100 Subject: [PATCH 11/20] refactor: test plugin app contains all test config --- .../config/app.php | 0 tests/TestPluginApp/config/bootstrap.php | 198 +++++++++++++++++ .../config/routes.php | 0 .../src/Application.php | 0 tests/bootstrap.php | 200 +----------------- tests/test_app/config/bootstrap.php | 2 - 6 files changed, 201 insertions(+), 199 deletions(-) rename tests/{test_app => TestPluginApp}/config/app.php (100%) create mode 100644 tests/TestPluginApp/config/bootstrap.php rename tests/{test_app => TestPluginApp}/config/routes.php (100%) rename tests/{test_app => TestPluginApp}/src/Application.php (100%) delete mode 100644 tests/test_app/config/bootstrap.php diff --git a/tests/test_app/config/app.php b/tests/TestPluginApp/config/app.php similarity index 100% rename from tests/test_app/config/app.php rename to tests/TestPluginApp/config/app.php diff --git a/tests/TestPluginApp/config/bootstrap.php b/tests/TestPluginApp/config/bootstrap.php new file mode 100644 index 0000000..52a5d64 --- /dev/null +++ b/tests/TestPluginApp/config/bootstrap.php @@ -0,0 +1,198 @@ + for more details. + */ + +use BEdita\Core\Filesystem\FilesystemRegistry; +use BEdita\Core\Plugin; +use Cake\Cache\Cache; +use Cake\Console\ConsoleErrorHandler; +use Cake\Core\Configure; +use Cake\Core\Configure\Engine\PhpConfig; +use Cake\Database\Type; +use Cake\Datasource\ConnectionManager; +use Cake\Error\ErrorHandler; +use Cake\Http\ServerRequest; +use Cake\Log\Log; +use Cake\Mailer\Email; +use Cake\Utility\Security; + +// Path constants to a few helpful things. +define('ROOT', dirname(dirname(dirname(__DIR__))) . DS); + +require_once ROOT . 'vendor/autoload.php'; + +define('CAKE_CORE_INCLUDE_PATH', ROOT . 'vendor' . DS . 'cakephp' . DS . 'cakephp'); +define('CORE_PATH', ROOT . 'vendor' . DS . 'cakephp' . DS . 'cakephp' . DS); +define('CAKE', CORE_PATH . 'src' . DS); +define('TESTS', ROOT . 'tests'); +define('APP', ROOT . 'tests' . DS . 'TestPluginApp' . DS); +define('APP_DIR', 'TestPluginApp'); +define('WEBROOT_DIR', 'webroot'); +define('WWW_ROOT', APP . 'webroot' . DS); +define('TMP', sys_get_temp_dir() . DS); +define('CONFIG', APP . 'config' . DS); +define('CACHE', TMP); +define('LOGS', TMP); + +$loader = new \Cake\Core\ClassLoader; +$loader->register(); + +require_once CORE_PATH . 'config/bootstrap.php'; + +date_default_timezone_set('UTC'); +mb_internal_encoding('UTF-8'); + +try { + Configure::config('default', new PhpConfig()); + Configure::load('app', 'default', false); +} catch (\Exception $e) { + exit($e->getMessage() . "\n"); +} + +if (!defined('API_KEY')) { + define('API_KEY', 'API_KEY'); +} + +// Ensure default test connection is defined +if (!getenv('db_dsn')) { + putenv('db_dsn=sqlite://127.0.0.1/' . TMP . 'plugin_test.sqlite'); +} + +ConnectionManager::drop('test'); +ConnectionManager::setConfig('test', ['url' => getenv('db_dsn')]); + +if (getenv('DEBUG_LOG_QUERIES')) { + ConnectionManager::get('test')->logQueries(true); + Log::setConfig('queries', [ + 'className' => 'Console', + 'stream' => 'php://stdout', + 'scopes' => ['queriesLog'], + ]); +} + +FilesystemRegistry::dropAll(); +Configure::write('Filesystem', [ + 'default' => [ + 'className' => 'BEdita/Core.Local', + 'path' => ROOT . 'vendor' . DS . 'bedita' . DS . 'core' . DS . 'tests' . DS . 'uploads', + 'baseUrl' => 'https://static.example.org/files', + ], +]); + +/* When debug = true the metadata cache should last + * for a very very short time, as we want + * to refresh the cache while developers are making changes. + */ +if (Configure::read('debug')) { + Configure::write('Cache._bedita_object_types_.duration', '+2 minutes'); + Configure::write('Cache._cake_model_.duration', '+2 minutes'); + Configure::write('Cache._cake_core_.duration', '+2 minutes'); +} + +/* + * Set server timezone to UTC. You can change it to another timezone of your + * choice but using UTC makes time calculations / conversions easier. + * Check http://php.net/manual/en/timezones.php for list of valid timezone strings. + */ +date_default_timezone_set('UTC'); + +/* + * Configure the mbstring extension to use the correct encoding. + */ +mb_internal_encoding(Configure::read('App.encoding')); + +/* + * Set the default locale. This controls how dates, number and currency is + * formatted and sets the default language to use for translations. + */ +ini_set('intl.default_locale', Configure::read('App.defaultLocale')); + +/* + * Register application error and exception handlers. + */ +$isCli = PHP_SAPI === 'cli'; +if ($isCli) { + (new ConsoleErrorHandler(Configure::read('Error')))->register(); +} else { + (new ErrorHandler(Configure::read('Error')))->register(); +} + +/* + * Include the CLI bootstrap overrides. + */ +if ($isCli) { + Configure::write('Log.debug.file', 'cli-debug'); + Configure::write('Log.error.file', 'cli-error'); +} + +/* + * Set the full base URL. + * This URL is used as the base of all absolute links. + * + * If you define fullBaseUrl in your config file you can remove this. + */ +if (!Configure::read('App.fullBaseUrl')) { + $s = null; + if (env('HTTPS')) { + $s = 's'; + } + + $httpHost = env('HTTP_HOST'); + if (isset($httpHost)) { + Configure::write('App.fullBaseUrl', 'http' . $s . '://' . $httpHost); + } + unset($httpHost, $s); +} + +Cache::setConfig(Configure::consume('Cache') ?: []); +Email::setConfigTransport(Configure::consume('EmailTransport') ?: []); +Email::setConfig(Configure::consume('Email') ?: []); +Log::setConfig(Configure::consume('Log') ?: []); +Security::setSalt((string)Configure::consume('Security.salt')); +FilesystemRegistry::setConfig(Configure::consume('Filesystem') ?: []); + +ServerRequest::addDetector('mobile', function ($request) { + $detector = new \Detection\MobileDetect(); + + return $detector->isMobile(); +}); +ServerRequest::addDetector('tablet', function ($request) { + $detector = new \Detection\MobileDetect(); + + return $detector->isTablet(); +}); + +Type::build('time') + ->useImmutable() + ->useLocaleParser(); +Type::build('date') + ->useImmutable() + ->useLocaleParser(); +Type::build('datetime') + ->useImmutable() + ->useLocaleParser(); + +Cache::drop('_bedita_object_types_'); +Cache::setConfig('_bedita_object_types_', ['className' => 'Null']); +Configure::write('debug', true); + +$basePluginsPath = ROOT . 'vendor' . DS . 'bedita' . DS; + +Plugin::load( + 'BEdita/Core', + ['bootstrap' => true, 'path' => $basePluginsPath . 'core' . DS] +); + +Plugin::load( + 'BEdita/API', + ['bootstrap' => true, 'routes' => true, 'path' => $basePluginsPath . 'api' . DS] +); diff --git a/tests/test_app/config/routes.php b/tests/TestPluginApp/config/routes.php similarity index 100% rename from tests/test_app/config/routes.php rename to tests/TestPluginApp/config/routes.php diff --git a/tests/test_app/src/Application.php b/tests/TestPluginApp/src/Application.php similarity index 100% rename from tests/test_app/src/Application.php rename to tests/TestPluginApp/src/Application.php diff --git a/tests/bootstrap.php b/tests/bootstrap.php index cc3e90e..00c4ca0 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -1,202 +1,8 @@ for more details. - */ -use BEdita\Core\Filesystem\FilesystemRegistry; -use BEdita\Core\Plugin; -use Cake\Cache\Cache; -use Cake\Console\ConsoleErrorHandler; -use Cake\Core\Configure; -use Cake\Core\Configure\Engine\PhpConfig; -use Cake\Database\Type; -use Cake\Datasource\ConnectionManager; -use Cake\Error\ErrorHandler; -use Cake\Http\ServerRequest; -use Cake\Log\Log; -use Cake\Mailer\Email; -use Cake\Utility\Security; +require_once 'TestPluginApp/config/bootstrap.php'; -require_once 'vendor/autoload.php'; - -// Path constants to a few helpful things. -define('ROOT', dirname(__DIR__) . DS); -define('CAKE_CORE_INCLUDE_PATH', ROOT . 'vendor' . DS . 'cakephp' . DS . 'cakephp'); -define('CORE_PATH', ROOT . 'vendor' . DS . 'cakephp' . DS . 'cakephp' . DS); -define('CAKE', CORE_PATH . 'src' . DS); -define('TESTS', ROOT . 'tests'); -define('APP', ROOT . 'tests' . DS . 'test_app' . DS); -define('APP_DIR', 'test_app'); -define('WEBROOT_DIR', 'webroot'); -define('WWW_ROOT', APP . 'webroot' . DS); -define('TMP', sys_get_temp_dir() . DS); -define('CONFIG', APP . 'config' . DS); -define('CACHE', TMP); -define('LOGS', TMP); - -$loader = new \Cake\Core\ClassLoader; -$loader->register(); - -require_once CORE_PATH . 'config/bootstrap.php'; - -date_default_timezone_set('UTC'); -mb_internal_encoding('UTF-8'); - -try { - Configure::config('default', new PhpConfig()); - Configure::load('app', 'default', false); -} catch (\Exception $e) { - exit($e->getMessage() . "\n"); -} - -if (!defined('API_KEY')) { - define('API_KEY', 'API_KEY'); -} - -// Ensure default test connection is defined -if (!getenv('db_dsn')) { - putenv('db_dsn=sqlite://127.0.0.1/' . TMP . 'devtools_test.sqlite'); -} - -ConnectionManager::drop('test'); -ConnectionManager::setConfig('test', ['url' => getenv('db_dsn')]); - -if (getenv('DEBUG_LOG_QUERIES')) { - ConnectionManager::get('test')->logQueries(true); - Log::setConfig('queries', [ - 'className' => 'Console', - 'stream' => 'php://stdout', - 'scopes' => ['queriesLog'], - ]); -} - -FilesystemRegistry::dropAll(); -Configure::write('Filesystem', [ - 'default' => [ - 'className' => 'BEdita/Core.Local', - 'path' => ROOT . 'vendor' . DS . 'bedita' . DS . 'core' . DS . 'tests' . DS . 'uploads', - 'baseUrl' => 'https://static.example.org/files', - ], -]); - -/* When debug = true the metadata cache should last - * for a very very short time, as we want - * to refresh the cache while developers are making changes. - */ -if (Configure::read('debug')) { - Configure::write('Cache._bedita_object_types_.duration', '+2 minutes'); - Configure::write('Cache._cake_model_.duration', '+2 minutes'); - Configure::write('Cache._cake_core_.duration', '+2 minutes'); -} - -/* - * Set server timezone to UTC. You can change it to another timezone of your - * choice but using UTC makes time calculations / conversions easier. - * Check http://php.net/manual/en/timezones.php for list of valid timezone strings. - */ -date_default_timezone_set('UTC'); - -/* - * Configure the mbstring extension to use the correct encoding. - */ -mb_internal_encoding(Configure::read('App.encoding')); - -/* - * Set the default locale. This controls how dates, number and currency is - * formatted and sets the default language to use for translations. - */ -ini_set('intl.default_locale', Configure::read('App.defaultLocale')); - -/* - * Register application error and exception handlers. - */ -$isCli = PHP_SAPI === 'cli'; -if ($isCli) { - (new ConsoleErrorHandler(Configure::read('Error')))->register(); -} else { - (new ErrorHandler(Configure::read('Error')))->register(); -} - -/* - * Include the CLI bootstrap overrides. - */ -if ($isCli) { - Configure::write('Log.debug.file', 'cli-debug'); - Configure::write('Log.error.file', 'cli-error'); -} - -/* - * Set the full base URL. - * This URL is used as the base of all absolute links. - * - * If you define fullBaseUrl in your config file you can remove this. - */ -if (!Configure::read('App.fullBaseUrl')) { - $s = null; - if (env('HTTPS')) { - $s = 's'; - } - - $httpHost = env('HTTP_HOST'); - if (isset($httpHost)) { - Configure::write('App.fullBaseUrl', 'http' . $s . '://' . $httpHost); - } - unset($httpHost, $s); -} - -Cache::setConfig(Configure::consume('Cache') ?: []); -Email::setConfigTransport(Configure::consume('EmailTransport') ?: []); -Email::setConfig(Configure::consume('Email') ?: []); -Log::setConfig(Configure::consume('Log') ?: []); -Security::setSalt((string)Configure::consume('Security.salt')); -FilesystemRegistry::setConfig(Configure::consume('Filesystem') ?: []); - -ServerRequest::addDetector('mobile', function ($request) { - $detector = new \Detection\MobileDetect(); - - return $detector->isMobile(); -}); -ServerRequest::addDetector('tablet', function ($request) { - $detector = new \Detection\MobileDetect(); - - return $detector->isTablet(); -}); - -Type::build('time') - ->useImmutable() - ->useLocaleParser(); -Type::build('date') - ->useImmutable() - ->useLocaleParser(); -Type::build('datetime') - ->useImmutable() - ->useLocaleParser(); - -Cache::drop('_bedita_object_types_'); -Cache::setConfig('_bedita_object_types_', ['className' => 'Null']); -Configure::write('debug', true); - -$basePluginsPath = ROOT . 'vendor' . DS . 'bedita' . DS; - -Plugin::load( - 'BEdita/Core', - ['bootstrap' => true, 'path' => $basePluginsPath . 'core' . DS] -); - -Plugin::load( - 'BEdita/API', - ['bootstrap' => true, 'routes' => true, 'path' => $basePluginsPath . 'api' . DS] -); - -Plugin::load( +\BEdita\Core\Plugin::load( 'BEdita/DevTools', - ['routes' => true, 'path' => ROOT] + ['bootstrap' => true, 'routes' => true, 'path' => ROOT] ); diff --git a/tests/test_app/config/bootstrap.php b/tests/test_app/config/bootstrap.php deleted file mode 100644 index 6abc49c..0000000 --- a/tests/test_app/config/bootstrap.php +++ /dev/null @@ -1,2 +0,0 @@ - Date: Sat, 9 Dec 2017 12:40:59 +0100 Subject: [PATCH 12/20] chore: test plugin app namespace --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index faaf9a9..dfdc978 100644 --- a/composer.json +++ b/composer.json @@ -38,7 +38,7 @@ "autoload-dev": { "psr-4": { "BEdita\\DevTools\\Test\\": "tests", - "BEdita\\App\\": "tests/test_app/src", + "BEdita\\App\\": "tests/TestPluginApp/src", "BEdita\\Core\\Test\\": "./vendor/bedita/core/tests", "BEdita\\API\\Test\\": "./vendor/bedita/api/tests", "Cake\\Test\\": "./vendor/cakephp/cakephp/tests" From 165c73bd58913da929f28cf1cba19dd421a9d3d3 Mon Sep 17 00:00:00 2001 From: stefanorosanelli Date: Sat, 9 Dec 2017 12:43:19 +0100 Subject: [PATCH 13/20] chore: restore code coverage via travis / codecov --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index 9347b71..46b5431 100644 --- a/.travis.yml +++ b/.travis.yml @@ -28,6 +28,8 @@ jobs: - php: 7.1 env: "DB=sqlite db_dsn='sqlite:///tmp/test.sql'" + script: vendor/bin/phpunit --coverage-clover=clover.xml + after_success: bash <(curl -s https://codecov.io/bash) - php: 7.0 env: "RUN=phpcs" From 8bbb0f73a0eee156f3766cb2f5160f2c4ed0a11b Mon Sep 17 00:00:00 2001 From: stefanorosanelli Date: Mon, 11 Dec 2017 17:22:01 +0100 Subject: [PATCH 14/20] feat: multiple files config + dynamic types --- config/OpenAPI/_open_api.php | 66 ++++++ config/OpenAPI/auth.php | 129 +++++++++++ .../{open_api.php => OpenAPI/components.php} | 208 +++++++----------- config/OpenAPI/templates.php | 160 ++++++++++++++ src/Spec/OpenAPI.php | 52 ++++- .../Controller/ToolsControllerTest.php | 1 + tests/TestCase/Spec/OpenAPITest.php | 1 + 7 files changed, 487 insertions(+), 130 deletions(-) create mode 100644 config/OpenAPI/_open_api.php create mode 100644 config/OpenAPI/auth.php rename config/{open_api.php => OpenAPI/components.php} (56%) create mode 100644 config/OpenAPI/templates.php diff --git a/config/OpenAPI/_open_api.php b/config/OpenAPI/_open_api.php new file mode 100644 index 0000000..b433b9e --- /dev/null +++ b/config/OpenAPI/_open_api.php @@ -0,0 +1,66 @@ + [ + 'openapi' => '3.0.0', + 'info' => [ + 'title' => '', // will be 'project name' + 'description' => 'Auto-generated specification', + 'version' => '', // will be 'BEdita version' + ], + 'servers' => [ + [ + 'url' => '', // will be 'project base URL' + ], + ], + 'paths' => [ + '/status' => [ + 'get' => [ + 'summary' => 'API Status', + 'description' => 'Service status response', + 'tags' => ['status'], + 'responses' => [ + '200' => [ + 'description' => '', + 'content' => [ + 'application/json' => [ + 'schema' => [ + '$ref' => '#/components/schemas/status' + ], + ] + ], + ], + '401' => [ + '$ref' => '#/components/responses/401', + ], + ], + ], + ], + '/home' => [ + 'get' => [ + 'summary' => 'API Home page', + 'description' => 'Information on avilable endpoints and methods', + 'tags' => ['home'], + 'responses' => [ + '200' => [ + 'description' => '', + 'content' => [ + 'application/json' => [ + 'schema' => [ + '$ref' => '#/components/schemas/home' + ], + ], + ], + ], + '401' => [ + '$ref' => '#/components/responses/401', + ], + ], + ], + ], + ], + 'components' => [], + ], +]; diff --git a/config/OpenAPI/auth.php b/config/OpenAPI/auth.php new file mode 100644 index 0000000..ab37250 --- /dev/null +++ b/config/OpenAPI/auth.php @@ -0,0 +1,129 @@ + [ + 'paths' => [ + '/auth' => [ + 'post' => [ + 'description' => "Authentication process and token renewal. + You do auth with POST /auth, passing auth data in as formData parameters. For instance: + + ``` + username: johndoe + password: ****** + ``` + + You renew token with POST /auth, using header parameter Authorization. For example: + + ``` + Authorization: 'Bearer eyJ0eXAiOi...2ljSerKQygk2T8' + ```", + 'summary' => 'Perform auth or renew token', + 'tags' => ['auth'], + + 'parameters' => [ + [ + 'in' => 'header', + 'name' => 'Authorization', + 'description' => "Use token prefixed with 'Bearer'", + 'required' => false, + 'type' => 'string', + ], + ], + + 'requestBody' => [ + 'description' => 'Login with username/password or renew auth token', + 'required' => false, + 'content' => [ + 'application/json' => [ + 'schema' => [ + '$ref' => '#/components/schemas/auth_login' + ], + ], + 'application/x-www-form-urlencoded' => [ + 'schema' => [ + '$ref' => '#/components/schemas/auth_login' + ], + ], + ], + ], + 'responses' => [ + '200' => [ + 'description' => 'Successful login', + 'content' => [ + 'application/json' => [ + 'schema' => [ + '$ref' => '#/components/schemas/auth_success' + ], + ], + ], + ], + '401' => [ + '$ref' => '#/components/responses/401', + ], + ], + ], + + 'get' => [ + 'summary' => 'Get logged user profile data', + 'tags' => ['auth'], + 'responses' => [ + '200' => [ + 'content' => [ + 'application/json' => [ + 'schema' => [ + '$ref' => '#/components/schemas/users' + ], + ], + ], + ], + '401' => [ + '$ref' => '#/components/responses/401', + ], + ], + ], + ], + ], + + // components used only in `auth/` + 'components' => [ + 'schemas' => [ + + 'auth_login' => [ + 'type' => 'object', + 'properties' => [ + 'username' => [ + 'type' => 'string', + ], + 'password' => [ + 'type' => 'string', + ], + ], + ], + + 'auth_success' => [ + 'type' => 'object', + 'properties' => [ + 'links' => [ + '$ref' => '#/components/schemas/links', + ], + 'meta' => [ + 'type' => 'object', + 'properties' => [ + 'jwt' => [ + 'type' => 'string', + ], + 'renew' => [ + 'type' => 'string', + ], + ], + ], + ], + ], + + ], + ] + ], +]; diff --git a/config/open_api.php b/config/OpenAPI/components.php similarity index 56% rename from config/open_api.php rename to config/OpenAPI/components.php index 9b25cbc..f44469b 100644 --- a/config/open_api.php +++ b/config/OpenAPI/components.php @@ -1,64 +1,9 @@ [ - 'openapi' => '3.0.0', - 'info' => [ - 'title' => '', // will be 'project name' - 'description' => 'Auto-generated specification', - 'version' => '', // will be 'BEdita version' - ], - 'servers' => [ - [ - 'url' => '', // will be 'project base URL' - ], - ], - 'paths' => [ - '/status' => [ - 'get' => [ - 'summary' => 'API Status', - 'description' => 'Service status response', - 'responses' => [ - '200' => [ - 'description' => '', - 'content' => [ - 'application/json' => [ - 'schema' => [ - '$ref' => '#/components/schemas/status' - ], - ] - ], - ], - '401' => [ - '$ref' => '#/components/responses/401', - ], - ], - ], - ], - '/home' => [ - 'get' => [ - 'summary' => 'API Home page', - 'description' => 'Information on avilable endpoints and methods', - 'responses' => [ - '200' => [ - 'description' => '', - 'content' => [ - 'application/json' => [ - 'schema' => [ - '$ref' => '#/components/schemas/home' - ], - ], - ], - ], - '401' => [ - '$ref' => '#/components/responses/401', - ], - ], - ], - ], - ], 'components' => [ 'securitySchemes' => [ 'apikey' => [ @@ -69,7 +14,7 @@ ], 'parameters' => [ - 'Id' => [ + 'id' => [ 'name' => 'id', 'in' => 'path', 'description' => 'numeric resource/object id', @@ -79,6 +24,24 @@ 'format' => 'int64', ], ], + 'q' => [ + 'name' => 'q', + 'in' => 'query', + 'description' => 'text search query on objects and resources', + 'required' => false, + 'schema' => [ + 'type' => 'string', + ], + ], + 'fields' => [ + 'name' => 'fields', + 'in' => 'query', + 'description' => 'filter response fields as comma separated values', + 'required' => false, + 'schema' => [ + 'type' => 'string', + ], + ], ], 'schemas' => [ @@ -95,6 +58,56 @@ ], ], + 'links_list' => [ + 'type' => 'object', + 'properties' => [ + 'self' => [ + 'type' => 'string', + ], + 'home' => [ + 'type' => 'string', + ], + 'first' => [ + 'type' => 'string', + ], + 'last' => [ + 'type' => 'string', + ], + 'prev' => [ + 'type' => 'string', + ], + 'next' => [ + 'type' => 'string', + ], + ], + ], + + 'meta_list' => [ + 'type' => 'object', + 'properties' => [ + 'pagination' => [ + 'type' => 'object', + 'properties' => [ + 'count' => [ + 'type' => 'integer', + ], + 'page' => [ + 'type' => 'integer', + ], + 'page_count' => [ + 'type' => 'integer', + ], + 'page_items' => [ + 'type' => 'integer', + ], + 'page_size' => [ + 'type' => 'integer', + ], + ], + ], + ], + ], + 'status' => [ 'type' => 'object', 'properties' => [ @@ -183,80 +196,29 @@ ], ], ], - ], - ], - ], - 'OATemplates' => [ - 'paths' => [ - '/{$resource}/{id}' => [ - 'get' => [ - 'summary' => '', - 'description' => '', - 'parameters' => [ - [ - '$ref' => '#/components/parameters/Id', - ], - ], - 'responses' => [ - '200' => [ - 'description' => 'Retrieve single "{$resource}"', - 'content' => [ - 'application/json' => [ - 'schema' => [ - '$ref' => '#/components/schemas/{$resource}' - ], - ] + '404' => [ + 'description' => 'Object or resource not found', + 'content' => [ + 'application/json' => [ + 'schema' => [ + '$ref' => '#/components/schemas/error' ], ], - '401' => [ - '$ref' => '#/components/responses/401', - ], ], ], - ], - ], - 'components' => [ - 'schemas' => [ - '{$resource}' => [ - 'type' => 'object', - 'properties' => [ - 'data' => [ - 'type' => 'object', - 'properties' => [ - 'id' => [ - 'type' => 'string', - ], - 'type' => [ - 'type' => 'string', - ], - 'attributes' => [ - 'type' => 'object', - 'properties' => [ - '$ref' => '#/components/schemas/{$resource}_attributes', - ], - ], - 'meta' => [ - 'type' => 'object', - 'properties' => [ - '$ref' => '#/components/schemas/{$resource}_meta', - ], - ], - 'relationships' => [ - 'type' => 'object', - 'properties' => [ - '$ref' => '#/components/schemas/{$resource}_relationships', - ], - ], - ] - ], - 'links' => [ - '$ref' => '#/components/schemas/links', + + '400' => [ + 'description' => 'Bad request', + 'content' => [ + 'application/json' => [ + 'schema' => [ + '$ref' => '#/components/schemas/error' + ], ], - ] + ], ], ], ], ], - ]; diff --git a/config/OpenAPI/templates.php b/config/OpenAPI/templates.php new file mode 100644 index 0000000..69d8eb0 --- /dev/null +++ b/config/OpenAPI/templates.php @@ -0,0 +1,160 @@ + [ + 'paths' => [ + '/{$resource}/{id}' => [ + 'get' => [ + 'summary' => 'Retrieve single "{$resource}"', + 'description' => '', + 'parameters' => [ + [ + '$ref' => '#/components/parameters/id', + ], + ], + 'tags' => ['objects'], + 'responses' => [ + '200' => [ + 'description' => '', + 'content' => [ + 'application/json' => [ + 'schema' => [ + '$ref' => '#/components/schemas/{$resource}' + ], + ] + ], + ], + '401' => [ + '$ref' => '#/components/responses/401', + ], + '404' => [ + '$ref' => '#/components/responses/404', + ], + ], + ], + ], + + '/{$resource}' => [ + 'get' => [ + 'summary' => 'Retrieve list of "{$resource}" with optional search, fields and custom filters', + 'description' => '', + 'parameters' => [ + [ + '$ref' => '#/components/parameters/q', + ], + [ + '$ref' => '#/components/parameters/fields', + ], + ], + 'tags' => ['objects'], + 'responses' => [ + '200' => [ + 'content' => [ + 'application/json' => [ + 'schema' => [ + '$ref' => '#/components/schemas/{$resource}_list' + ], + ] + ], + ], + '401' => [ + '$ref' => '#/components/responses/401', + ], + '404' => [ + '$ref' => '#/components/responses/404', + ], + ], + ], + ], + ], + 'components' => [ + 'schemas' => [ + + '{$resource}' => [ + 'type' => 'object', + 'properties' => [ + 'data' => [ + 'type' => 'object', + 'properties' => [ + 'id' => [ + 'type' => 'string', + ], + 'type' => [ + 'type' => 'string', + ], + 'attributes' => [ + 'type' => 'object', + 'properties' => [ + '$ref' => '#/components/schemas/{$resource}_attributes', + ], + ], + 'meta' => [ + 'type' => 'object', + 'properties' => [ + '$ref' => '#/components/schemas/{$resource}_meta', + ], + ], + 'relationships' => [ + 'type' => 'object', + 'properties' => [ + '$ref' => '#/components/schemas/{$resource}_relationships', + ], + ], + ] + ], + 'links' => [ + '$ref' => '#/components/schemas/links', + ], + ], + ], + + '{$resource}_list' => [ + 'type' => 'object', + 'properties' => [ + 'data' => [ + 'type' => 'array', + 'items' => [ + 'type' => 'object', + 'properties' => [ + 'id' => [ + 'type' => 'string', + ], + 'type' => [ + 'type' => 'string', + ], + 'attributes' => [ + 'type' => 'object', + 'properties' => [ + '$ref' => '#/components/schemas/{$resource}_attributes', + ], + ], + 'meta' => [ + 'type' => 'object', + 'properties' => [ + '$ref' => '#/components/schemas/{$resource}_meta', + ], + ], + 'relationships' => [ + 'type' => 'object', + 'properties' => [ + '$ref' => '#/components/schemas/{$resource}_relationships', + ], + ], + ], + ], + ], + 'links' => [ + '$ref' => '#/components/schemas/links_list', + ], + 'meta' => [ + '$ref' => '#/components/schemas/meta_list', + ], + ], + ], + + ], + ], + ], +]; diff --git a/src/Spec/OpenAPI.php b/src/Spec/OpenAPI.php index 4e23547..0386f40 100644 --- a/src/Spec/OpenAPI.php +++ b/src/Spec/OpenAPI.php @@ -15,6 +15,9 @@ use BEdita\Core\Model\Schema\JsonSchema; use Cake\Core\Configure; +use Cake\Core\Plugin; +use Cake\Filesystem\Folder; +use Cake\ORM\TableRegistry; use Cake\Routing\Router; use Cake\Utility\Hash; use Symfony\Component\Yaml\Yaml; @@ -33,6 +36,16 @@ class OpenAPI */ protected static $types = []; + /** + * Resources described by the spec + * + * @var array + */ + const RESOURCES = [ + 'roles', + 'streams', + ]; + /** * Generate OpenAPI v3 project as PHP array. * @@ -40,7 +53,7 @@ class OpenAPI */ public static function generate() { - Configure::load('BEdita/DevTools.open_api'); + static::loadConfigurations(); $spec = Configure::read('OpenAPI'); $spec['info']['title'] = Configure::read('Project.name') . ' API'; @@ -54,6 +67,22 @@ public static function generate() return $spec; } + /** + * Load OpenAPI configuration files. + * + * @return void + */ + protected static function loadConfigurations() + { + // load configuration files + $path = Plugin::path('BEdita/DevTools') . 'config/OpenAPI'; + $dir = new Folder($path); + $files = $dir->find('.*\.php', true); + foreach ($files as $file) { + Configure::load('BEdita/DevTools.OpenAPI/' . substr($file, 0, strlen($file) - 4)); + } + } + /** * Generate OpenAPI v3 project as YAML string. * @@ -82,6 +111,12 @@ protected static function dynamicPaths() foreach ($paths as $url => $data) { $path = str_replace('{$resource}', $t, $url); $data = json_decode(str_replace('{$resource}', $t, json_encode($data)), true); + // replace 'tags' -> `objects` with `resources` + if (in_array($t, static::RESOURCES)) { + array_walk($data, function (&$values) { + $values['tags'] = ['resources']; + }); + } $res[$path] = $data; } } @@ -97,7 +132,8 @@ protected static function dynamicPaths() protected static function availableTypes() { if (empty(static::$types)) { - static::$types = ['roles', 'objects', 'users']; + $objectTypes = TableRegistry::get('ObjectTypes')->find('list', ['valueField' => 'name'])->toArray(); + static::$types = array_merge($objectTypes, static::RESOURCES); } return static::$types; @@ -113,12 +149,14 @@ protected static function dynamicSchemas() $res = []; $schemas = Configure::read('OATemplates.components.schemas'); $types = static::availableTypes(); - foreach ($types as $t) { - $template = json_decode(str_replace('{$resource}', $t, json_encode($schemas['{$resource}'])), true); - $res[$t] = $template; - $schema = static::retrieveSchema($t); + foreach ($types as $type) { + foreach ($schemas as $name => $data) { + $schemaName = str_replace('{$resource}', $type, $name); + $res[$schemaName] = json_decode(str_replace('{$resource}', $type, json_encode($data)), true); + } + $schema = static::retrieveSchema($type); foreach (['attributes', 'meta', 'relationships'] as $item) { - $res[sprintf('%s_%s', $t, $item)] = $schema[$item]; + $res[sprintf('%s_%s', $type, $item)] = $schema[$item]; } } diff --git a/tests/TestCase/Controller/ToolsControllerTest.php b/tests/TestCase/Controller/ToolsControllerTest.php index ed92515..1f3b858 100644 --- a/tests/TestCase/Controller/ToolsControllerTest.php +++ b/tests/TestCase/Controller/ToolsControllerTest.php @@ -32,6 +32,7 @@ class ToolsControllerTest extends IntegrationTestCase 'plugin.BEdita/Core.locations', 'plugin.BEdita/Core.property_types', 'plugin.BEdita/Core.properties', + 'plugin.BEdita/Core.streams', ]; /** diff --git a/tests/TestCase/Spec/OpenAPITest.php b/tests/TestCase/Spec/OpenAPITest.php index 35e66a5..875357b 100644 --- a/tests/TestCase/Spec/OpenAPITest.php +++ b/tests/TestCase/Spec/OpenAPITest.php @@ -53,6 +53,7 @@ class OpenAPITest extends TestCase * @covers ::generate() * @covers ::dynamicPaths() * @covers ::dynamicSchemas() + * @covers ::loadConfigurations() * @covers ::availableTypes() * @covers ::retrieveSchema() */ From fb222dfad3fb62287479bae6398c3d839adc839e Mon Sep 17 00:00:00 2001 From: stefanorosanelli Date: Mon, 11 Dec 2017 17:30:24 +0100 Subject: [PATCH 15/20] test: improve coverage - clear() method --- src/Panel/ConfigurationPanel.php | 1 + src/Spec/OpenAPI.php | 12 +++++++++++- tests/TestCase/Spec/OpenAPITest.php | 16 +++++++++++++++- 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/src/Panel/ConfigurationPanel.php b/src/Panel/ConfigurationPanel.php index 065c55a..9c5d28a 100644 --- a/src/Panel/ConfigurationPanel.php +++ b/src/Panel/ConfigurationPanel.php @@ -33,6 +33,7 @@ class ConfigurationPanel extends DebugPanel * Collect configuration data when panel is initialized. * * @return void + * @codeCoverageIgnore */ public function initialize() { diff --git a/src/Spec/OpenAPI.php b/src/Spec/OpenAPI.php index 0386f40..32414b0 100644 --- a/src/Spec/OpenAPI.php +++ b/src/Spec/OpenAPI.php @@ -129,7 +129,7 @@ protected static function dynamicPaths() * * @return array OpenAPI dynamic paths */ - protected static function availableTypes() + public static function availableTypes() { if (empty(static::$types)) { $objectTypes = TableRegistry::get('ObjectTypes')->find('list', ['valueField' => 'name'])->toArray(); @@ -139,6 +139,16 @@ protected static function availableTypes() return static::$types; } + /** + * Clear internal registry + * + * @return void + */ + public static function clear() + { + self::$types = []; + } + /** * Add dynamic `components.schemas` from template * diff --git a/tests/TestCase/Spec/OpenAPITest.php b/tests/TestCase/Spec/OpenAPITest.php index 875357b..cc990c7 100644 --- a/tests/TestCase/Spec/OpenAPITest.php +++ b/tests/TestCase/Spec/OpenAPITest.php @@ -54,7 +54,6 @@ class OpenAPITest extends TestCase * @covers ::dynamicPaths() * @covers ::dynamicSchemas() * @covers ::loadConfigurations() - * @covers ::availableTypes() * @covers ::retrieveSchema() */ public function testGenerate() @@ -84,4 +83,19 @@ public function testGenerateYaml() $expectedKeys = ['openapi', 'info', 'servers', 'paths', 'components']; static::assertEquals($expectedKeys, array_keys($data)); } + + /** + * Test `availableTypes` method + * + * @return void + * + * @covers ::availableTypes() + * @covers ::clear() + */ + public function testAvailableTypes() + { + OpenAPI::clear(); + $result = OpenAPI::availableTypes(); + static::assertNotEmpty($result); + } } From c95b4379798b7f9957a9a7892e45c7e7d9d2444d Mon Sep 17 00:00:00 2001 From: stefanorosanelli Date: Mon, 11 Dec 2017 17:59:18 +0100 Subject: [PATCH 16/20] chore: docblock updates [ci skip] --- config/OpenAPI/_open_api.php | 13 ++++++++++++- config/OpenAPI/{auth.php => auth_endpoint.php} | 3 ++- config/OpenAPI/templates.php | 9 ++++++++- config/routes.php | 2 +- src/Spec/OpenAPI.php | 3 +++ 5 files changed, 26 insertions(+), 4 deletions(-) rename config/OpenAPI/{auth.php => auth_endpoint.php} (99%) diff --git a/config/OpenAPI/_open_api.php b/config/OpenAPI/_open_api.php index b433b9e..c1adf96 100644 --- a/config/OpenAPI/_open_api.php +++ b/config/OpenAPI/_open_api.php @@ -1,7 +1,18 @@ [ 'openapi' => '3.0.0', diff --git a/config/OpenAPI/auth.php b/config/OpenAPI/auth_endpoint.php similarity index 99% rename from config/OpenAPI/auth.php rename to config/OpenAPI/auth_endpoint.php index ab37250..eb44c5e 100644 --- a/config/OpenAPI/auth.php +++ b/config/OpenAPI/auth_endpoint.php @@ -1,7 +1,7 @@ [ 'paths' => [ @@ -89,6 +89,7 @@ // components used only in `auth/` 'components' => [ + 'schemas' => [ 'auth_login' => [ diff --git a/config/OpenAPI/templates.php b/config/OpenAPI/templates.php index 69d8eb0..f88b7f3 100644 --- a/config/OpenAPI/templates.php +++ b/config/OpenAPI/templates.php @@ -1,7 +1,14 @@ [ 'paths' => [ diff --git a/config/routes.php b/config/routes.php index 13f753d..81072b9 100644 --- a/config/routes.php +++ b/config/routes.php @@ -9,7 +9,7 @@ 'path' => '/tools', ], function (RouteBuilder $routes) { - // OpenAPI 3 spec in YAML format. + // OpenAPI 3 spec in YAML or JSON format. $routes->connect( '/open-api', ['controller' => 'Tools', 'action' => 'openApi', 'method' => 'GET'] diff --git a/src/Spec/OpenAPI.php b/src/Spec/OpenAPI.php index 32414b0..47d5726 100644 --- a/src/Spec/OpenAPI.php +++ b/src/Spec/OpenAPI.php @@ -25,6 +25,9 @@ /** * OpenAPI generation utility methods. * + * Spec generation is driven by configuration files in `config/OpenAPI`. + * Have look at `config/OpenAPI/_open_api.php` for a brief description. + * * @since 4.0.0 */ class OpenAPI From 3705008583c56bbba3fd35d65b52fc4ed50c55ed Mon Sep 17 00:00:00 2001 From: stefanorosanelli Date: Wed, 13 Dec 2017 13:14:26 +0100 Subject: [PATCH 17/20] [minor] chore: typo on description [ci skip] --- config/OpenAPI/_open_api.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/OpenAPI/_open_api.php b/config/OpenAPI/_open_api.php index c1adf96..0c7d3c3 100644 --- a/config/OpenAPI/_open_api.php +++ b/config/OpenAPI/_open_api.php @@ -52,7 +52,7 @@ '/home' => [ 'get' => [ 'summary' => 'API Home page', - 'description' => 'Information on avilable endpoints and methods', + 'description' => 'Information on available endpoints and methods', 'tags' => ['home'], 'responses' => [ '200' => [ From 77c8eb93152ff23a103b2d21afd172e3e21e82ef Mon Sep 17 00:00:00 2001 From: stefanorosanelli Date: Wed, 13 Dec 2017 18:15:34 +0100 Subject: [PATCH 18/20] chore: avoid plugin version conflicts for now - use `*` temporary --- composer.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index dfdc978..1d74c12 100644 --- a/composer.json +++ b/composer.json @@ -21,8 +21,8 @@ } ], "require": { - "bedita/core": "dev-4-cactus", - "bedita/api": "dev-4-cactus", + "bedita/core": "*", + "bedita/api": "*", "cakephp/debug_kit": "~3.10" }, "require-dev": { From 9cfef2e08cd460540d9579d2ac970ac4daa9c1e5 Mon Sep 17 00:00:00 2001 From: stefanorosanelli Date: Fri, 26 Jun 2020 19:15:30 +0200 Subject: [PATCH 19/20] fix: update JsonSchema namespace --- src/Spec/OpenAPI.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Spec/OpenAPI.php b/src/Spec/OpenAPI.php index 47d5726..07b866c 100644 --- a/src/Spec/OpenAPI.php +++ b/src/Spec/OpenAPI.php @@ -13,13 +13,12 @@ namespace BEdita\DevTools\Spec; -use BEdita\Core\Model\Schema\JsonSchema; +use BEdita\Core\Utility\JsonSchema; use Cake\Core\Configure; use Cake\Core\Plugin; use Cake\Filesystem\Folder; use Cake\ORM\TableRegistry; use Cake\Routing\Router; -use Cake\Utility\Hash; use Symfony\Component\Yaml\Yaml; /** From 864161e2009a7abbd3d88141f25f5cbfa5724404 Mon Sep 17 00:00:00 2001 From: stefanorosanelli Date: Tue, 14 Jul 2020 19:17:51 +0200 Subject: [PATCH 20/20] fix: replace deprecated `url` attribute --- src/Middleware/HtmlMiddleware.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Middleware/HtmlMiddleware.php b/src/Middleware/HtmlMiddleware.php index 9456d86..297f8b5 100644 --- a/src/Middleware/HtmlMiddleware.php +++ b/src/Middleware/HtmlMiddleware.php @@ -38,7 +38,7 @@ class HtmlMiddleware */ public function __invoke(ServerRequestInterface $request, ResponseInterface $response, callable $next) { - $paths = explode('/', $request->url); + $paths = explode('/', $request->getUri()->getPath()); if (!($request instanceof ServerRequest) || !$request->is('html') || $paths[0] === 'tools') { // Not an HTML request, or unable to detect easily. return $next($request, $response);