diff --git a/.travis.yml b/.travis.yml index d145f8b..898cf85 100644 --- a/.travis.yml +++ b/.travis.yml @@ -45,9 +45,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. diff --git a/composer.json b/composer.json index 294cedc..cfb9172 100644 --- a/composer.json +++ b/composer.json @@ -37,6 +37,9 @@ "autoload-dev": { "psr-4": { "BEdita\\DevTools\\Test\\": "tests", + "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" } }, diff --git a/config/OpenAPI/_open_api.php b/config/OpenAPI/_open_api.php new file mode 100644 index 0000000..0c7d3c3 --- /dev/null +++ b/config/OpenAPI/_open_api.php @@ -0,0 +1,77 @@ + [ + '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 available 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_endpoint.php b/config/OpenAPI/auth_endpoint.php new file mode 100644 index 0000000..eb44c5e --- /dev/null +++ b/config/OpenAPI/auth_endpoint.php @@ -0,0 +1,130 @@ + [ + '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/OpenAPI/components.php b/config/OpenAPI/components.php new file mode 100644 index 0000000..f44469b --- /dev/null +++ b/config/OpenAPI/components.php @@ -0,0 +1,224 @@ + [ + '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', + ], + ], + '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' => [ + + 'links' => [ + 'type' => 'object', + 'properties' => [ + 'self' => [ + 'type' => 'string', + ], + 'home' => [ + 'type' => 'string', + ], + ], + ], + + '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' => [ + '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' + ], + ], + ], + ], + + '404' => [ + 'description' => 'Object or resource not found', + 'content' => [ + 'application/json' => [ + 'schema' => [ + '$ref' => '#/components/schemas/error' + ], + ], + ], + ], + + '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..f88b7f3 --- /dev/null +++ b/config/OpenAPI/templates.php @@ -0,0 +1,167 @@ + [ + '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/config/routes.php b/config/routes.php new file mode 100644 index 0000000..81072b9 --- /dev/null +++ b/config/routes.php @@ -0,0 +1,20 @@ + '/tools', + ], + function (RouteBuilder $routes) { + // OpenAPI 3 spec in YAML or JSON 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..7bd28b9 --- /dev/null +++ b/src/Controller/ToolsController.php @@ -0,0 +1,91 @@ + for more details. + */ +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} + * + * @codeCoverageIgnore + */ + 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) + { + } + + /** + * Return OpenAPI spec in JSON or YAML + * + * @return \Cake\Http\Response + */ + public function openApi() + { + $this->request->allowMethod('get'); + + 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'); + + if ($this->request->is('html')) { + return $this->render(); + } + + return $this->render()->withType(static::YAML_CONTENT_TYPE); + } +} diff --git a/src/Middleware/HtmlMiddleware.php b/src/Middleware/HtmlMiddleware.php index 9474cef..297f8b5 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->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); } 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 new file mode 100644 index 0000000..07b866c --- /dev/null +++ b/src/Spec/OpenAPI.php @@ -0,0 +1,202 @@ + for more details. + */ + +namespace BEdita\DevTools\Spec; + +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 Symfony\Component\Yaml\Yaml; + +/** + * 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 +{ + /** + * Available resource and object types described by the spec + * + * @var array + */ + protected static $types = []; + + /** + * Resources described by the spec + * + * @var array + */ + const RESOURCES = [ + 'roles', + 'streams', + ]; + + /** + * Generate OpenAPI v3 project as PHP array. + * + * @return array + */ + public static function generate() + { + static::loadConfigurations(); + $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; + } + + /** + * 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. + * + * @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); + // replace 'tags' -> `objects` with `resources` + if (in_array($t, static::RESOURCES)) { + array_walk($data, function (&$values) { + $values['tags'] = ['resources']; + }); + } + $res[$path] = $data; + } + } + + return $res; + } + + /** + * Available types + * + * @return array OpenAPI dynamic paths + */ + public static function availableTypes() + { + if (empty(static::$types)) { + $objectTypes = TableRegistry::get('ObjectTypes')->find('list', ['valueField' => 'name'])->toArray(); + static::$types = array_merge($objectTypes, static::RESOURCES); + } + + return static::$types; + } + + /** + * Clear internal registry + * + * @return void + */ + public static function clear() + { + self::$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 $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', $type, $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/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..1701d38 --- /dev/null +++ b/src/Template/OpenAPI/yaml.ctp @@ -0,0 +1 @@ + diff --git a/tests/TestCase/Controller/ToolsControllerTest.php b/tests/TestCase/Controller/ToolsControllerTest.php new file mode 100644 index 0000000..1f3b858 --- /dev/null +++ b/tests/TestCase/Controller/ToolsControllerTest.php @@ -0,0 +1,84 @@ + 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', + 'plugin.BEdita/Core.streams', + ]; + + /** + * 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..cc990c7 --- /dev/null +++ b/tests/TestCase/Spec/OpenAPITest.php @@ -0,0 +1,101 @@ + 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 ::loadConfigurations() + * @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)); + } + + /** + * Test `availableTypes` method + * + * @return void + * + * @covers ::availableTypes() + * @covers ::clear() + */ + public function testAvailableTypes() + { + OpenAPI::clear(); + $result = OpenAPI::availableTypes(); + static::assertNotEmpty($result); + } +} diff --git a/tests/TestPluginApp/config/app.php b/tests/TestPluginApp/config/app.php new file mode 100644 index 0000000..c3ba544 --- /dev/null +++ b/tests/TestPluginApp/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/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/TestPluginApp/config/routes.php b/tests/TestPluginApp/config/routes.php new file mode 100644 index 0000000..33c0f27 --- /dev/null +++ b/tests/TestPluginApp/config/routes.php @@ -0,0 +1,5 @@ +add(ErrorHandlerMiddleware::class) + + // Handle plugin/theme assets like CakePHP normally does. + ->add(AssetMiddleware::class) + + // Add routing middleware. + ->add(new RoutingMiddleware($this)); + + return $middlewareQueue; + } +} diff --git a/tests/test_app/config/routes.php b/tests/test_app/config/routes.php deleted file mode 100644 index 0eeb4df..0000000 --- a/tests/test_app/config/routes.php +++ /dev/null @@ -1,14 +0,0 @@ - for more details. - */ - -// Do nothing. :) diff --git a/tests/Fixture/.gitkeep b/tmp/.gitkeep similarity index 100% rename from tests/Fixture/.gitkeep rename to tmp/.gitkeep